CLI Plugin Manager SubCommand (#3414)

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2018-09-25 10:29:18 -04:00 committed by GitHub
parent 76a688a747
commit 0ced18841f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1312 additions and 24 deletions

View file

@ -5,9 +5,9 @@ AllCops:
- Gemfile
- Rakefile
- 'test/**/*'
- 'lib/plugins/*/test/**/*'
- 'examples/**/*'
- 'vendor/**/*'
- 'lib/plugins/inspec-*/test/**/*'
- 'lib/bundles/inspec-init/templates/**/*'
- 'www/demo/**/*'
AlignParameters:

View file

@ -26,9 +26,11 @@ The software design of the InSpec Plugin v2 API is deeply inspired by the Vagran
The normal distribution and installation method is via gems, handled by the `inspec plugin` command.
TODO: give basic overview of `inspec plugin` and link to docs
`inspec plugin install inspec-myplugin` will fetch `inspec-myplugin` from rubygems.org, and install it and its gemspec dependencies under the user's `.inspec` directory. You may also provide a local gemfile. For local development, however, path-to-source is usually most convenient.
### Plugins may also be found by path
For more on the `plugin` CLI command, run `inspec plugin help`.
### Plugins may also be found by path to a source tree
For local development or site-specific installations, you can also 'install' a plugin by path using `inspec plugin`, or edit `~/.inspec/plugins.json` directly to add a plugin.

46
docs/plugins.md Normal file
View file

@ -0,0 +1,46 @@
---
title: About InSpec Plugins
---
# InSpec Plugins
## What are InSpec Plugins?
Plugins are optional software components that extend the capabilities of InSpec. For example, `inspec-iggy` is a plugin project that aims to generate InSpec controls from infrastructure-as-code. Plugins are distributed as RubyGems, and InSpec manages their installation.
## What can plugins do?
Currently, each plugin can offer one or more of these capabilities:
* define a new command-line-interface (CLI) command suite
Future work might include new capability types, such as:
* connectivity to new types of hosts or cloud providers (`train` transports)
* reporters (output generators)
* DSL extensions at the file, control, or test level
* attribute fetchers to allow reading InSpec attributes from new sources (for example, a remote, encrypted key-value store)
## How do I find out which plugins are available?
The InSpec CLI can tell you which plugins are available:
```bash
$ inspec plugin search
```
## How do I install and manage plugins?
The InSpec command line now offers a new subcommand just for managing plugins.
You can install a plugin by running:
```bash
$ inspec plugin install inspec-some-plugin
```
For more details on what the `plugin` command can do, see the [online help](https://www.inspec.io/docs/reference/cli/#plugin), or run `inspec plugin help`.
## How do I write a plugin?
For details on how to author a Plugin, see the [developer documentation](https://github.com/inspec/inspec/blob/master/docs/dev/plugins.md)

View file

@ -47,4 +47,5 @@ Gem::Specification.new do |spec|
spec.add_dependency 'semverse'
spec.add_dependency 'htmlentities'
spec.add_dependency 'multipart-post'
spec.add_dependency 'term-ansicolor'
end

View file

@ -2,6 +2,7 @@
require 'singleton'
require 'forwardable'
require 'fileutils'
# Gem extensions for doing unusual things - not loaded by Gem default
require 'rubygems/package'
@ -114,6 +115,7 @@ module Inspec::Plugin::V2
# @param [String] plugin_seach_term
# @param [Hash] opts Search options
# @option opts [TrueClass, FalseClass] :exact If true, use plugin_search_term exactly. If false (default), append a wildcard.
# @option opts [Symbol] :scope Which versions to search for. :released (default) - all released versions. :prerelease - Also include versioned marked prerelease. :latest - only return one version, the latest one.
# @return [Hash of Arrays] - Keys are String names of gems, arrays contain String versions.
def search(plugin_query, opts = {})
validate_search_opts(plugin_query, opts)
@ -121,10 +123,10 @@ module Inspec::Plugin::V2
fetcher = Gem::SpecFetcher.fetcher
matched_tuples = []
if opts[:exact]
matched_tuples = fetcher.detect(:released) { |tuple| tuple.name == plugin_query }
matched_tuples = fetcher.detect(opts[:scope]) { |tuple| tuple.name == plugin_query }
else
regex = Regexp.new('^' + plugin_query + '.*')
matched_tuples = fetcher.detect(:released) do |tuple|
matched_tuples = fetcher.detect(opts[:scope]) do |tuple|
tuple.name != 'inspec-core' && tuple.name =~ regex
end
end
@ -179,8 +181,8 @@ module Inspec::Plugin::V2
raise InstallError, "Could not find local gem file to install - #{opts[:gem_file]}"
end
elsif opts.key?(:path)
unless Dir.exist?(opts[:path])
raise InstallError, "Could not find directory for install from source path - #{opts[:path]}"
unless File.exist?(opts[:path])
raise InstallError, "Could not find path for install from source path - #{opts[:path]}"
end
end
@ -226,10 +228,15 @@ module Inspec::Plugin::V2
end
end
def validate_search_opts(search_term, _opts)
def validate_search_opts(search_term, opts)
unless search_term =~ /^(inspec|train)-/
raise SearchError, "All inspec plugins must begin with either 'inspec-' or 'train-'."
end
opts[:scope] ||= :released
unless [:prerelease, :released, :latest].include?(opts[:scope])
raise SearchError, 'Search scope for listing versons must be :prerelease, :released, or :latest.'
end
end
#===================================================================#
@ -276,6 +283,18 @@ module Inspec::Plugin::V2
# OK, perform the installation.
# Ignore deps here, because any needed deps should already be baked into new_plugin_dependency
request_set.install_into(gem_path, true, ignore_dependencies: true)
# Painful aspect of rubygems: the VendorSet request set type needs to be able to find a gemspec
# file within the source of the gem (and not all gems include it in their source tree; they are
# 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
end
#===================================================================#
@ -367,6 +386,7 @@ module Inspec::Plugin::V2
# TODO: refactor the plugin.json file to have its own class, which Installer consumes
def update_plugin_config_file(plugin_name, opts)
config = update_plugin_config_data(plugin_name, opts)
FileUtils.mkdir_p(Inspec.config_dir)
File.write(plugin_conf_file_path, JSON.pretty_generate(config))
end

View file

@ -112,17 +112,25 @@ module Inspec::Plugin::V2
# Lists all gems found in the plugin_gem_path.
# @return [Array[Gem::Specification]] Specs of all gems found.
def list_managed_gems
def self.list_managed_gems
Dir.glob(File.join(plugin_gem_path, 'specifications', '*.gemspec')).map { |p| Gem::Specification.load(p) }
end
def list_managed_gems
self.class.list_managed_gems
end
# Lists all plugin gems found in the plugin_gem_path.
# This is simply all gems that begin with train- or inspec-.
# @return [Array[Gem::Specification]] Specs of all gems found.
def list_installed_plugin_gems
def self.list_installed_plugin_gems
list_managed_gems.select { |spec| spec.name.match(/^(inspec|train)-/) }
end
def list_installed_plugin_gems
self.class.list_managed_gems
end
# TODO: refactor the plugin.json file to have its own class, which Loader consumes
def plugin_conf_file_path
self.class.plugin_conf_file_path
@ -200,7 +208,7 @@ module Inspec::Plugin::V2
File.join(bundle_dir, 'train-*.rb'),
]
Dir.glob(globs).each do |loader_file|
name = File.basename(loader_file, '.rb').gsub(/^(inspec|train)-/, '')
name = File.basename(loader_file, '.rb').to_sym
status = Inspec::Plugin::V2::Status.new
status.name = name
status.entry_point = loader_file
@ -216,9 +224,9 @@ module Inspec::Plugin::V2
# with lib/ dirs, etc.
Dir.glob(File.join(core_plugins_dir, 'inspec-*')).each do |plugin_dir|
status = Inspec::Plugin::V2::Status.new
status.name = File.basename(plugin_dir)
status.entry_point = File.join(plugin_dir, 'lib', status.name + '.rb')
status.installation_type = :path
status.name = File.basename(plugin_dir).to_sym
status.entry_point = File.join(plugin_dir, 'lib', status.name.to_s + '.rb')
status.installation_type = :core
status.loaded = false
registry[status.name.to_sym] = status
end

View file

@ -64,7 +64,7 @@ module Inspec::Plugin::V2
def register(name, status)
if known_plugin? name
Inspec::Log.warn "PluginLoader: refusing to re-register plugin '#{name}': an existing plugin with that name was loaded via #{existing.installation_type}-loading from #{existing.entry_point}"
Inspec::Log.debug "PluginLoader: refusing to re-register plugin '#{name}': an existing plugin with that name was loaded via #{registry[name].installation_type}-loading from #{registry[name].entry_point}"
else
registry[name.to_sym] = status
end

View file

@ -0,0 +1,6 @@
# InSpec Plugin Manager CLI
This is a CLI plugin for InSpec. It uses the Plugins API v2 to create a
series of commands to manage plugins.
It was the first plugin to be authored as a core plugin under Plugins v2.

View file

@ -0,0 +1,18 @@
# Because this is a core plugin, we place the plugin definition here in the entry point.
# This is needed because under core testing, the entry point may be reloaded multiple times,
# and we need plugin registration to properly occur each time.
# More typically, the entry point would just load a plugin definition file.
module InspecPlugins
module PluginManager
class Plugin < Inspec.plugin(2)
plugin_name :'inspec-plugin-manager-cli'
cli_command :plugin do
require_relative 'inspec-plugin-manager-cli/cli_command'
InspecPlugins::PluginManager::CliCommand
end
end
end
end

View file

@ -0,0 +1,394 @@
require 'term/ansicolor'
require 'pathname'
require 'inspec/plugin/v2/installer'
module InspecPlugins
module PluginManager
class CliCommand < Inspec.plugin(2, :cli_command)
include Term::ANSIColor
subcommand_desc 'plugin SUBCOMMAND', 'Manage InSpec plugins'
#==================================================================#
# inspec plugin list
#==================================================================#
desc 'list [options]', 'Lists user-installed InSpec plugins.'
option :all, desc: 'Include plugins shipped with InSpec as well.', type: :boolean, aliases: [:a]
def list
plugin_statuses = Inspec::Plugin::V2::Registry.instance.plugin_statuses
plugin_statuses.reject! { |s| [:core, :bundle].include?(s.installation_type) } unless options[:all]
# TODO: ui object support
puts
puts(bold { format(' %-30s%-10s%-8s%-6s', 'Plugin Name', 'Version', 'Via', 'ApiVer') })
puts '-' * 55
plugin_statuses.sort_by(&:name).each do |status|
puts(format(' %-30s%-10s%-8s%-6s', status.name, make_pretty_version(status), status.installation_type, status.api_generation.to_s))
end
puts '-' * 55
puts(" #{plugin_statuses.count} plugin(s) total")
puts
end
#==================================================================#
# inspec plugin search
#==================================================================#
desc 'search [options] PATTERN', 'Searches rubygems.org for InSpec plugins. Exits 0 on a search hit, exits 2 on a search miss.'
option :all, desc: 'List all available versions, not just the latest one.', type: :boolean, aliases: [:a]
option :exact, desc: 'Assume PATTERN is exact; do not add a wildcard to the end', type: :boolean, aliases: [:e]
# Justification for disabling ABC: currently at 33.51/33
def search(search_term) # rubocop: disable Metrics/AbcSize
search_results = installer.search(search_term, exact: options[:exact])
# TODO: ui object support
puts
puts(bold { format(' %-30s%-50s%', 'Plugin Name', 'Versions Available') })
puts '-' * 55
search_results.keys.sort.each do |plugin_name|
versions = options[:all] ? search_results[plugin_name] : [search_results[plugin_name].first]
versions = '(' + versions.join(', ') + ')'
puts(format(' %-30s%-50s', plugin_name, versions))
end
puts '-' * 55
puts(" #{search_results.count} plugin(s) found")
puts
exit 2 if search_results.empty?
rescue Inspec::Plugin::V2::SearchError => ex
Inspec::Log.error ex.message
exit 1
end
#==================================================================#
# inspec plugin install
#==================================================================#
desc 'install [-v VERSION] PLUGIN', 'Installs a plugin from rubygems.org, a gemfile, or a path to local source.'
long_desc <<~EOLD
PLUGIN may be the name of a gem on rubygems.org that begins with inspec- or train-.
PLUGIN may also be the path to a local gemfile, which will then be installed like
any other gem. Finally, if PLUGIN is a path ending in .rb, it is taken to be a
local file that will act as athe entry point for a plugin (this mode is provided
for local plugin development). Exit codes are 0 on success, 2 if the plugin is
already installed, and 1 if any other error occurs.
EOLD
option :version, desc: 'When installing from rubygems.org, specifies a specific version to install.', aliases: [:v]
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
#--------------------------
# update
#--------------------------
desc 'update PLUGIN', 'Updates a plugin to the latest from from rubygems.org'
long_desc <<~EOLD
PLUGIN may be the name of a gem on rubygems.org that begins with inspec- or train-.
Exit codes are 0 on success, 2 if the plugin is already up to date, and 1 if any
other error occurs.
EOLD
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
puts(red { 'Update error: ' } + ex.message + ' - update failed')
exit 1
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
puts(bold { plugin_name } + " plugin, version #{old_version} -> #{new_version}, updated from rubygems.org")
end
#--------------------------
# uninstall
#--------------------------
desc 'uninstall PLUGIN_NAME', 'Uninstalls a gem- or path- based plugin'
long_desc <<~EOLD
Removes a plugin from the users configuration.
In the case of a gem plugin (by far the most common), the plugin gem is removed, along
with any of its dependencies that are no longer needed by anything else. Finally, the
plugin configuration file is updated to reflect that the plugin is no longer present.
In the case of a path-based plugin (often used for plugin development), no changes
are made to the referenced plugin source code. Rather, the plugin's entry is simply removed
from the plugin config file.
EOLD
def uninstall(plugin_name)
status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym]
unless status
puts(red { 'No such plugin installed: ' } + "#{plugin_name} is not installed - uninstall failed")
exit 1
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
puts(bold { plugin_name } + ' path-based plugin install has been uninstalled')
else
puts(bold { plugin_name } + " plugin, version #{old_version}, has been uninstalled")
end
exit 0
end
private
#==================================================================#
# install breakdown
#==================================================================#
# These are broken down because rubocop complained.
def install_from_gemfile(gem_file)
unless File.exist? gem_file
puts(red { 'No such plugin gem file ' } + gem_file + ' - installation failed.')
exit 1
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)
puts(bold { plugin_name } + " plugin, version #{version}, installed from local .gem file")
exit 0
end
def install_from_path(path)
unless File.exist? path
puts(red { 'No such source code path ' } + path + ' - installation failed.')
exit 1
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)
puts(red { 'Plugin already installed' } + " - #{plugin_name} - Use 'inspec plugin list' to see previously installed plugin - installation failed.")
exit 2
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)
puts(bold { plugin_name } + ' plugin installed via source path reference, resolved to entry point ' + entry_point)
exit 0
end
# 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.
def install_from_path__apply_entry_point_heuristics(path) # rubocop: disable Metrics/AbcSize, Metrics/CyclomaticComplexity
given = Pathname.new(path)
given = given.expand_path # Resolve any relative paths
name_regex = /^(inspec|train)-/
# What are the last four things like?
parts = [
given.parent.parent.basename,
given.parent.basename,
given.basename('.rb'),
given.extname,
].map(&:to_s)
# 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
# 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
# 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.
puts(red { 'Unrecognizable plugin structure' } + " - #{parts[2]} - When installing from a path, please provide the path of the entry point file - installation failed.")
exit 1
end
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
puts(red { 'Plugin contains errors' } + " - #{plugin_name} - Encountered errors while trying to test load the plugin entry point, resolved to #{entry_point} - installation failed")
puts ex.message
exit 1
end
# OK, the wheels didn't fall off. But is it a plugin?
unless Inspec::Plugin::V2::Registry.instance.known_plugin?(plugin_name.to_sym)
puts(red { 'Does not appear to be a plugin' } + " - #{plugin_name} - After probe-loading the supposed plugin, it did not register itself. Ensure something inherits from 'Inspec.plugin(2)' - installation failed.")
exit 1
end
end
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
puts(bold { plugin_name } + " plugin, version #{new_version}, installed from rubygems.org")
exit 0
end
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
puts(red { 'Plugin already installed at requested version' } + " - plugin #{plugin_name} #{requested_version} - refusing to install.")
elsif what_we_would_install_is_already_installed && !they_explicitly_asked_for_a_version
puts(red { 'Plugin already installed at latest version' } + " - plugin #{plugin_name} #{requested_version} - refusing to install.")
else
# There are existing versions installed, but none of them are what was requested
puts(red { 'Update required' } + " - plugin #{plugin_name}, requested #{requested_version}, have #{pre_installed_versions.join(', ')}; use `inspec plugin update` - refusing to install.")
end
exit 2
end
def install_attempt_install(plugin_name)
installer.install(plugin_name, version: options[:version])
rescue Inspec::Plugin::V2::InstallError
results = installer.search(plugin_name, exact: true)
if results.empty?
puts(red { 'No such plugin gem ' } + plugin_name + ' could be found on rubygems.org - installation failed.')
elsif options[:version] && !results[plugin_name].include?(options[:version])
puts(red { 'No such version' } + ' - ' + plugin_name + " exists, but no such version #{options[:version]} found on rubygems.org - installation failed.")
else
puts(red { 'Unknown error occured ' } + ' - installation failed.')
end
exit 1
end
#==================================================================#
# update breakdown
#==================================================================#
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
puts(red { 'No such plugin installed: ' } + "#{plugin_name} - update failed")
exit 1
elsif status.installation_type == :path
puts(red { 'Cannot update path-based install: ' } + "#{plugin_name} is installed via path reference; use `inspec plugin uninstall` to remove - refusing to update")
exit 2
end
end
# Check for latest version (and implicitly, existance)
latest_version = installer.search(plugin_name, exact: true, scope: :latest)
latest_version = latest_version[plugin_name]&.last
if pre_update_versions.include?(latest_version)
puts(red { 'Already installed at latest version: ' } + "#{plugin_name} is at #{latest_version}, which the latest - refusing to update")
exit 2
end
end
#==================================================================#
# utilities
#==================================================================#
def installer
Inspec::Plugin::V2::Installer.instance
end
def registry
Inspec::Plugin::V2::Registry.instance
end
def check_plugin_name(plugin_name, action)
unless plugin_name =~ /^(inspec|train)-/
puts(red { 'Invalid plugin name' } + " - #{plugin_name} - All inspec plugins must begin with either 'inspec-' or 'train-' - #{action} failed.")
exit 1
end
end
def make_pretty_version(status)
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 :path
'src'
end
end
end
end
end

View file

@ -0,0 +1,12 @@
module InspecPlugins
module PluginManager
class Plugin < Inspec.plugin(2)
plugin_name :'inspec-plugin-manager-cli'
cli_command :plugin do
require_relative 'cli'
InspecPlugins::PluginManager::CliCommand
end
end
end
end

View file

@ -0,0 +1,2 @@
# Despite having the right file structure, and the right name,
# this is not actually an InSpec plugin.

View file

@ -0,0 +1 @@
# This should should never even be loaded.

View file

@ -0,0 +1,577 @@
#=========================================================================================#
# `inspec plugin SUBCOMMAND` facility
#=========================================================================================#
require_relative '../../../shared/core_plugin_test_helper.rb'
#-----------------------------------------------------------------------------------------#
# utilities
#-----------------------------------------------------------------------------------------#
module PluginManagerHelpers
let(:project_repo_path) { File.expand_path(File.join(__FILE__, '..', '..', '..')) }
let(:project_fixtures_path) { File.join(project_repo_path, 'test', 'fixtures') }
let(:project_config_dirs_path) { File.join(project_fixtures_path, 'config_dirs') }
let(:empty_config_dir_path) { File.join(project_config_dirs_path, 'empty') }
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 })
end
end
def copy_in_project_config_dir(fixture_name, dest = nil)
src = Dir.glob(File.join(project_config_dirs_path, fixture_name, '*'))
dest ||= File.join(project_config_dirs_path, 'empty')
src.each { |path| FileUtils.cp_r(path, dest) }
end
def copy_in_core_config_dir(fixture_name, dest = nil)
src = Dir.glob(File.join(core_config_dir_path, fixture_name, '*'))
dest ||= File.join(project_config_dirs_path, 'empty')
src.each { |path| FileUtils.cp_r(path, dest) }
end
def clear_empty_config_dir
Dir.glob(File.join(project_config_dirs_path, 'empty', '*')).each do |path|
next if path.end_with? '.gitkeep'
FileUtils.rm_rf(path)
end
end
def teardown
clear_empty_config_dir
end
end
#-----------------------------------------------------------------------------------------#
# inspec help
#-----------------------------------------------------------------------------------------#
class PluginManagerCliHelp < MiniTest::Test
include CorePluginFunctionalHelper
# Main inspec help subcommand listing
def test_inspec_help_includes_plugin
result = run_inspec_process_with_this_plugin('help')
assert_includes result.stdout, 'inspec plugin'
end
# inspec plugin help subcommand listing
def test_inspec_plugin_help_includes_plugin
result = run_inspec_process_with_this_plugin('plugin help')
assert_includes result.stdout, 'inspec plugin list'
assert_includes result.stdout, 'inspec plugin search'
assert_includes result.stdout, 'inspec plugin install'
assert_includes result.stdout, 'inspec plugin update'
assert_includes result.stdout, 'inspec plugin uninstall'
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin list
#-----------------------------------------------------------------------------------------#
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')
assert_equal 0, result.exit_status, 'exist status must be 0'
assert_includes result.stdout, '0 plugin(s) total', 'Empty list should include zero count'
end
def test_list_all_when_no_user_plugins_installed
result = run_inspec_process_with_this_plugin('plugin list --all')
assert_equal 0, result.exit_status, 'exist status must be 0'
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'
result = run_inspec_process_with_this_plugin('plugin list -a')
assert_equal 0, result.exit_status, 'exist status must be 0'
assert_includes result.stdout, '6 plugin(s) total', '-a list should find six'
end
def test_list_when_gem_and_path_plugins_installed
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
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)
assert_equal 0, result.exit_status, 'exist status must be 0'
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
# -------------------------------------------------------
# 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)
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin search
#-----------------------------------------------------------------------------------------#
class PluginManagerCliSearch < MiniTest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_search_for_a_real_gem_with_full_name_no_options
result = run_inspec_process('plugin search inspec-test-fixture')
assert_equal 0, result.exit_status, 'Search should exit 0 on a hit'
assert_includes result.stdout, 'inspec-test-fixture', 'Search result should contain the gem name'
assert_includes result.stdout, '1 plugin(s) found', 'Search result should find 1 plugin'
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/,line,'Plugin line should include name and exactly one version')
end
def test_search_for_a_real_gem_with_stub_name_no_options
result = run_inspec_process('plugin search inspec-test-')
assert_equal 0, result.exit_status, 'Search should exit 0 on a hit'
assert_includes result.stdout, 'inspec-test-fixture', 'Search result should contain the gem name'
assert_includes result.stdout, '1 plugin(s) found', 'Search result should find 1 plugin'
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/,line,'Plugin line should include name and exactly one version')
end
def test_search_for_a_real_gem_with_full_name_and_exact_option
result = run_inspec_process('plugin search --exact inspec-test-fixture')
assert_equal 0, result.exit_status, 'Search should exit 0 on a hit'
assert_includes result.stdout, 'inspec-test-fixture', 'Search result should contain the gem name'
assert_includes result.stdout, '1 plugin(s) found', 'Search result should find 1 plugin'
result = run_inspec_process('plugin search -e inspec-test-fixture')
assert_equal 0, result.exit_status, 'Search should exit 0 on a hit'
end
def test_search_for_a_real_gem_with_stub_name_and_exact_option
result = run_inspec_process('plugin search --exact inspec-test-')
assert_equal 2, result.exit_status, 'Search should exit 2 on a miss'
assert_includes result.stdout, '0 plugin(s) found', 'Search result should find 0 plugins'
result = run_inspec_process('plugin search -e inspec-test-')
assert_equal 2, result.exit_status, 'Search should exit 2 on a miss'
end
def test_search_for_a_real_gem_with_full_name_and_all_option
result = run_inspec_process('plugin search --all inspec-test-fixture')
assert_equal 0, result.exit_status, 'Search should exit 0 on a hit'
assert_includes result.stdout, 'inspec-test-fixture', 'Search result should contain the gem name'
assert_includes result.stdout, '1 plugin(s) found', 'Search result should find 1 plugin'
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+(,\s)?){2,}\)/,line,'Plugin line should include name and at least two versions')
result = run_inspec_process('plugin search -a inspec-test-fixture')
assert_equal 0, result.exit_status, 'Search should exit 0 on a hit'
end
def test_search_for_a_gem_with_missing_prefix
result = run_inspec_process('plugin search test-fixture')
assert_equal 1, result.exit_status, 'Search should exit 1 on user error'
assert_includes result.stdout, "All inspec plugins must begin with either 'inspec-' or 'train-'"
end
def test_search_for_a_gem_that_does_not_exist
result = run_inspec_process('plugin search inspec-test-fixture-nonesuch')
assert_equal 2, result.exit_status, 'Search should exit 2 on a miss'
assert_includes result.stdout, '0 plugin(s) found', 'Search result should find 0 plugins'
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin install
#-----------------------------------------------------------------------------------------#
class PluginManagerCliInstall < MiniTest::Test
include CorePluginFunctionalHelper # gives us instance methods, like `let` aliases inside test methods
extend CorePluginFunctionalHelper # gives us class methods, like `let` aliases out here outside test methods
include PluginManagerHelpers
# Test multiple hueristics of the path-mode install.
# These are all positive tests; they should resolve the entry point to the same path in each case.
{
'is_perfect' => File.join(core_fixture_plugins_path, 'inspec-test-fixture', 'lib', 'inspec-test-fixture.rb'),
'refers_to_the_entry_point_with_no_extension' => File.join(core_fixture_plugins_path, 'inspec-test-fixture', 'lib', 'inspec-test-fixture'),
'refers_to_the_src_root_of_a_plugin' => File.join(core_fixture_plugins_path, 'inspec-test-fixture'),
'refers_to_a_relative_path' => File.join('test', 'unit', 'mock', 'plugins', 'inspec-test-fixture', 'lib', 'inspec-test-fixture.rb'),
}.each do |test_name, fixture_plugin_path|
define_method(('test_install_from_path_when_path_' + test_name).to_sym) do
install_result = run_inspec_process_with_this_plugin("plugin install #{fixture_plugin_path}", post_run: list_after_run)
assert_empty install_result.stderr
assert_equal 0, install_result.exit_status, 'Exit status should be 0'
# Check UX messaging
success_message = install_result.stdout.split("\n").grep(/installed/).last
refute_nil success_message, 'Should find a success message at the end'
assert_includes success_message, 'inspec-test-fixture'
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(/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+src\s+path\s+/, itf_line, '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'] == 'inspec-test-fixture' }
expected = File.join(core_fixture_plugins_path, 'inspec-test-fixture', 'lib', 'inspec-test-fixture.rb')
assert_equal expected, entry['installation_path'], 'Regardless of input, the entry point should be correct.'
end
end
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}")
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
error_message = install_result.stdout.split("\n").last
assert_includes error_message, "No such source code path"
assert_includes error_message, 'inspec-test-fixture-nonesuch.rb'
assert_includes error_message, 'installation failed'
end
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}")
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
error_message = install_result.stdout.split("\n").last
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-'"
assert_includes error_message, 'installation failed'
end
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}")
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
error_message = install_result.stdout.split("\n").last
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"
assert_includes error_message, "Ensure something inherits from 'Inspec.plugin(2)'"
assert_includes error_message, 'installation failed'
end
def test_fail_install_from_path_when_it_is_already_installed
plugin_path = File.join(core_fixture_plugins_path, 'inspec-test-fixture', 'lib', 'inspec-test-fixture.rb')
pre_block = Proc.new do |plugin_data, _tmp_dir|
plugin_data["plugins"] << {
"name" => "inspec-test-fixture",
"installation_type" => "path",
"installation_path" => plugin_path,
}
end
install_result = run_inspec_process_with_this_plugin("plugin install #{plugin_path}", pre_run: pre_block)
assert_empty install_result.stderr
assert_equal 2, install_result.exit_status, 'Exit status on second install should be 2'
error_message = install_result.stdout.split("\n").last
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"
assert_includes error_message, 'installation failed'
end
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}")
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
error_message = install_result.stdout.split("\n").last
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'
assert_includes error_message, 'installation failed'
end
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)
assert_empty install_result.stderr
assert_equal 0, install_result.exit_status, 'Exit status should be 0'
success_message = install_result.stdout.split("\n").grep(/installed/).last
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')
end
def test_fail_install_from_nonexistant_gemfile
bad_path = File.join(project_fixtures_path, 'none', 'such', 'inspec-test-fixture-nonesuch-0.3.0.gem')
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
assert_match(/No such plugin gem file .+ - installation failed./, install_result.stdout)
end
def test_install_from_rubygems_latest
install_result = run_inspec_process_with_this_plugin('plugin install inspec-test-fixture', post_run: list_after_run)
assert_empty install_result.stderr
assert_equal 0, install_result.exit_status, 'Exit status should be 0'
success_message = install_result.stdout.split("\n").grep(/installed/).last
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')
end
def test_fail_install_from_nonexistant_remote_rubygem
install_result = run_inspec_process_with_this_plugin('plugin install inspec-test-fixture-nonesuch')
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
assert_match(/No such plugin gem .+ could be found on rubygems.org - installation failed./, install_result.stdout)
end
def test_install_from_rubygems_with_pinned_version
install_result = run_inspec_process_with_this_plugin('plugin install inspec-test-fixture -v 0.1.0', post_run: list_after_run)
assert_empty install_result.stderr
assert_equal 0, install_result.exit_status, 'Exit status should be 0'
success_message = install_result.stdout.split("\n").grep(/installed/).last
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 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')
end
def test_fail_install_from_nonexistant_rubygem_version
install_result = run_inspec_process_with_this_plugin('plugin install inspec-test-fixture -v 99.99.99')
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
fail_message = install_result.stdout.split("\n").grep(/failed/).last
refute_nil fail_message, 'Should find a failure message at the end'
assert_includes fail_message, 'inspec-test-fixture'
assert_includes fail_message, '99.99.99'
assert_includes fail_message, 'no such version'
assert_includes fail_message, 'on rubygems.org'
end
def test_refuse_install_when_missing_prefix
install_result = run_inspec_process_with_this_plugin('plugin install test-fixture')
assert_empty install_result.stderr
assert_equal 1, install_result.exit_status, 'Exit status should be 1'
fail_message = install_result.stdout.split("\n").grep(/failed/).last
refute_nil fail_message, 'Should find a failure message at the end'
assert_includes fail_message, 'test-fixture'
assert_includes fail_message, "All inspec plugins must begin with either 'inspec-' or 'train-'"
end
def test_refuse_install_when_already_installed_same_version
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir('test-fixture-2-float', tmp_dir)
end
install_result = run_inspec_process_with_this_plugin('plugin install inspec-test-fixture', pre_run: pre_block)
assert_empty install_result.stderr
assert_equal 2, install_result.exit_status, 'Exit status should be 2'
refusal_message = install_result.stdout.split("\n").grep(/refusing/).last
refute_nil refusal_message, 'Should find a failure message at the end'
assert_includes refusal_message, 'inspec-test-fixture'
assert_includes refusal_message, '0.2.0'
assert_includes refusal_message, 'Plugin already installed at latest version'
end
def test_refuse_install_when_already_installed_can_update
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir('test-fixture-1-float', tmp_dir)
end
install_result = run_inspec_process_with_this_plugin('plugin install inspec-test-fixture', pre_run: pre_block)
assert_empty install_result.stderr
assert_equal 2, install_result.exit_status, 'Exit status should be 2'
refusal_message = install_result.stdout.split("\n").grep(/refusing/).last
refute_nil refusal_message, 'Should find a failure message at the end'
assert_includes refusal_message, 'inspec-test-fixture'
assert_includes refusal_message, '0.1.0'
assert_includes refusal_message, '0.2.0'
assert_includes refusal_message, 'Update required'
assert_includes refusal_message, 'inspec plugin update'
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin update
#-----------------------------------------------------------------------------------------#
class PluginManagerCliUpdate < MiniTest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_when_a_plugin_can_be_updated
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir('test-fixture-1-float', tmp_dir)
end
update_result = run_inspec_process_with_this_plugin('plugin update inspec-test-fixture', pre_run: pre_block, post_run: list_after_run)
assert_empty update_result.stderr
assert_equal 0, update_result.exit_status, 'Exit status should be 0'
success_message = update_result.stdout.split("\n").grep(/updated/).last
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, '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')
end
def test_refuse_update_when_already_current
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir('test-fixture-2-float', tmp_dir)
end
update_result = run_inspec_process_with_this_plugin('plugin update inspec-test-fixture', pre_run: pre_block)
assert_empty update_result.stderr
assert_equal 2, update_result.exit_status, 'Exit status should be 2'
refusal_message = update_result.stdout.split("\n").grep(/refusing/).last
refute_nil refusal_message, 'Should find a failure message at the end'
assert_includes refusal_message, 'inspec-test-fixture'
assert_includes refusal_message, '0.2.0'
assert_includes refusal_message, 'Already installed at latest version'
end
def test_fail_update_from_nonexistant_gem
update_result = run_inspec_process_with_this_plugin('plugin update inspec-test-fixture-nonesuch')
assert_empty update_result.stderr
assert_equal 1, update_result.exit_status, 'Exit status should be 1'
assert_match(/No such plugin installed: .+ - update failed/, update_result.stdout)
end
def test_fail_update_path
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir('meaning_by_path', tmp_dir)
end
update_result = run_inspec_process_with_this_plugin('plugin update inspec-meaning-of-life', pre_run: pre_block)
assert_empty update_result.stderr
assert_equal 2, update_result.exit_status, 'Exit status should be 2'
refusal_message = update_result.stdout.split("\n").grep(/refusing/).last
refute_nil refusal_message, 'Should find a failure message at the end'
assert_includes refusal_message, 'inspec-meaning-of-life'
assert_includes refusal_message, 'inspec plugin uninstall'
assert_includes refusal_message, 'Cannot update path-based install'
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin uninstall
#-----------------------------------------------------------------------------------------#
class PluginManagerCliUninstall < MiniTest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_when_a_gem_plugin_can_be_uninstalled
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir('test-fixture-1-float', tmp_dir)
end
# Attempt uninstall
uninstall_result = run_inspec_process_with_this_plugin('plugin uninstall inspec-test-fixture', pre_run: pre_block, post_run: list_after_run)
assert_empty uninstall_result.stderr
assert_equal 0, uninstall_result.exit_status, 'Exit status should be 0'
success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last
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, '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'
end
def test_when_a_path_plugin_can_be_uninstalled
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
# 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)
assert_empty uninstall_result.stderr
assert_equal 0, uninstall_result.exit_status, 'Exit status should be 0'
success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last
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'
end
def test_fail_uninstall_from_plugin_that_is_not_installed
uninstall_result = run_inspec_process_with_this_plugin('plugin uninstall inspec-test-fixture-nonesuch')
assert_empty uninstall_result.stderr
assert_equal 1, uninstall_result.exit_status, 'Exit status should be 1'
refute_includes 'Inspec::Plugin::V2::UnInstallError', uninstall_result.stdout # Stacktrace marker
assert_match(/No such plugin installed: .+ - uninstall failed/, uninstall_result.stdout)
end
end

View file

@ -0,0 +1,71 @@
require_relative '../../../shared/core_plugin_test_helper.rb'
#-----------------------------------------------------------------------#
# Thor option defs
#-----------------------------------------------------------------------#
class PluginManagerCliOptions < MiniTest::Test
include CorePluginUnitHelper
let(:cli_class) { InspecPlugins::PluginManager::CliCommand }
def setup
require_relative '../../lib/inspec-plugin-manager-cli/cli_command'
end
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_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'
assert_equal 0, cli_class.instance_method(:list).arity, 'The list command should take no arguments'
end
def test_search_args
arg_config = cli_class.all_commands['search'].options
assert_equal 2, arg_config.count, 'The search command should have 2 options'
assert_includes arg_config.keys, :all, 'The search 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'
assert_includes arg_config.keys, :exact, 'The search command should have an --exact option'
assert_equal :boolean, arg_config[:exact].type, 'The --exact option should be boolean'
assert_equal :e, arg_config[:exact].aliases.first, 'The --exact option should be aliased as -e'
refute_nil arg_config[:exact].description, 'The --exact option should have a description'
refute arg_config[:exact].required, 'The --exact option should not be required'
assert_equal 1, cli_class.instance_method(:search).arity, 'The search command should take one argument'
end
def test_install_args
arg_config = cli_class.all_commands['install'].options
assert_equal 1, arg_config.count, 'The install command should have 1 option'
assert_includes arg_config.keys, :version, 'The install command should have a --version option'
assert_equal :string, arg_config[:version].type, 'The --version option should be a string'
assert_equal :v, arg_config[:version].aliases.first, 'The --version option should be aliased as -v'
refute_nil arg_config[:version].description, 'The --version option should have a description'
refute arg_config[:version].required, 'The --version option should not be required'
assert_equal 1, cli_class.instance_method(:install).arity, 'The install command should take one argument'
end
def test_update_args
# TODO: allow specifying version
arg_config = cli_class.all_commands['update'].options
assert_equal 0, arg_config.count, 'The update command should have no options'
assert_equal 1, cli_class.instance_method(:update).arity, 'The update command should take one argument'
end
def test_uninstall_args
arg_config = cli_class.all_commands['uninstall'].options
assert_equal 0, arg_config.count, 'The uninstall command should have no options'
assert_equal 1, cli_class.instance_method(:uninstall).arity, 'The uninstall command should take one argument'
end
end

View file

@ -0,0 +1,20 @@
require_relative '../../../shared/core_plugin_test_helper.rb'
#-----------------------------------------------------------------------#
# Plugin Definition
#-----------------------------------------------------------------------#
class PluginManagerCliDefinitionTests < MiniTest::Test
include CorePluginUnitHelper
def test_plugin_registered
loader = Inspec::Plugin::V2::Loader.new
loader.load_all # We want to ensure it is auto-loaded
assert registry.known_plugin?(:'inspec-plugin-manager-cli'), 'inspec-plugin-manager-cli should be registered'
assert registry.loaded_plugin?(:'inspec-plugin-manager-cli'), 'inspec-plugin-manager-cli should be loaded'
status = registry[:'inspec-plugin-manager-cli']
assert_equal 2, status.api_generation, 'inspec-plugin-manager-cli should be v2'
assert_includes status.plugin_types, :cli_command, 'inspec-plugin-manager-cli should have cli_command activators'
end
end

View file

@ -11,12 +11,32 @@ require 'ostruct'
# Utilities often needed
require 'fileutils'
require 'tmpdir'
require 'pathname'
require 'forwardable'
# Configure MiniTest to expose things like `let`
class Module
include Minitest::Spec::DSL
end
module Inspec
class FuncTestRunResult
attr_reader :train_result
attr_reader :payload
extend Forwardable
def_delegator :train_result, :stdout
def_delegator :train_result, :stderr
def_delegator :train_result, :exit_status
def initialize(train_result)
@train_result = train_result
@payload = OpenStruct.new
end
end
end
module CorePluginBaseHelper
let(:repo_path) { File.expand_path(File.join(__FILE__, '..', '..', '..', '..')) }
let(:inspec_bin_path) { File.join(repo_path, 'bin', 'inspec') }
@ -40,11 +60,90 @@ module CorePluginFunctionalHelper
elsif opts.key?(:env)
prefix = opts[:env].to_a.map { |assignment| "#{assignment[0]}=#{assignment[1]}" }.join(' ')
end
TRAIN_CONNECTION.run_command("#{prefix} #{inspec_bin_path} #{command_line}")
Inspec::FuncTestRunResult.new(TRAIN_CONNECTION.run_command("#{prefix} #{inspec_bin_path} #{command_line}"))
end
# This helper does some fancy footwork to make InSpec think a plugin
# under development is temporarily installed.
# @param String command_line Invocation, without the word 'inspec'
# @param Hash opts options as for run_inspec_process, with more options:
# :pre_run: Proc(plugin_statefile_data, tmp_dir_path) - optional setup block.
# Modify plugin_statefile_data as needed; it will be written to a plugins.json
# in tmp_dir_path. You may also copy in other things to the tmp_dir_path. Your PWD
# will be in the tmp_dir, and it will exist and be empty.
# :post_run: Proc(FuncTestRunResult, tmp_dir_path) - optional result capture block.
# run_result will be populated, but you can add more to the ostruct .payload
# Your PWD will be the tmp_dir, and it will still exist (for a moment!)
def run_inspec_process_with_this_plugin(command_line, opts = {})
plugin_path = __find_plugin_path_from_caller
# If it looks like it is a core plugin under test, don't add it to the plugin file
# since the loader will auto-load it anyway
if plugin_path.include?('lib/plugins/inspec-')
plugin_file_data = __make_empty_plugin_file_data_structure
else
plugin_file_data = __make_plugin_file_data_structure_with_path(plugin_path)
end
Dir.mktmpdir do |tmp_dir|
opts[:pre_run]&.call(plugin_file_data, tmp_dir)
plugin_file_path = File.join(tmp_dir, 'plugins.json')
# HACK: If the block cleared the hash, take that to mean it will provide a plugins.json file of its own.
File.write(plugin_file_path, JSON.generate(plugin_file_data)) unless plugin_file_data.empty?
opts[:env] ||= {}
opts[:env]['INSPEC_CONFIG_DIR'] = tmp_dir
run_result = run_inspec_process(command_line, opts)
# Read the resulting plugins.json into memory, if any
if File.exist?(plugin_file_path)
run_result.payload.plugin_data = JSON.parse(File.read(plugin_file_path))
end
opts[:post_run]&.call(run_result, tmp_dir)
run_result
end
end
def __find_plugin_path_from_caller(frames_back = 2)
caller_path = Pathname.new(caller_locations(frames_back, 1).first.absolute_path)
# Typical caller path:
# /Users/cwolfe/sandbox/inspec-resource-lister/test/functional/inspec_resource_lister_test.rb
# We want:
# /Users/cwolfe/sandbox/inspec-resource-lister/lib/inspec-resource-lister.rb
cursor = caller_path
until cursor.basename.to_s == 'test' && cursor.parent.basename.to_s =~ /^(inspec|train)-/
cursor = cursor.parent
break if cursor.nil?
end
raise 'Could not comprehend plugin project directory structure' if cursor.nil?
project_dir = cursor.parent
plugin_name = project_dir.basename
entry_point = File.join(project_dir.to_s, 'lib', plugin_name.to_s + '.rb')
raise 'Could not find plugin entry point' unless File.exist?(entry_point)
entry_point
end
def __make_plugin_file_data_structure_with_path(path)
# TODO: dry this up, refs #3350
plugin_name = File.basename(path, '.rb')
data = __make_empty_plugin_file_data_structure
data['plugins'] << {
'name' => plugin_name,
'installation_type' => 'path',
'installation_path' => path,
}
end
def __make_empty_plugin_file_data_structure
# TODO: dry this up, refs #3350
{
'plugins_config_version' => '1.0.0',
'plugins' => [],
}
end
end
module CorePluginUnitHelper
include CorePluginBaseHelper
require 'inspec'
end

View file

@ -80,3 +80,9 @@ describe 'cli command plugins' do
outcome.stdout.must_include 'inspec meaningoflife'
end
end
#=========================================================================================#
# inspec plugin command
#=========================================================================================#
# See lib/plugins/inspec-plugin-manager-cli/test

View file

@ -3,6 +3,11 @@
"plugins": [
{
"name": "inspec-test-fixture"
},
{
"name": "inspec-meaning-of-life",
"installation_type": "path",
"installation_path": "test/unit/mock/plugins/meaning_of_life_path_mode/inspec-meaning-of-life"
}
]
}

View file

@ -1,2 +1,2 @@
require 'inspec-test-fixture/version'
require 'inspec-test-fixture/plugin'
require_relative 'inspec-test-fixture/version'
require_relative 'inspec-test-fixture/plugin'

View file

@ -5,7 +5,7 @@ module InspecPlugins
plugin_name :'inspec-test-fixture'
mock_plugin_type :'inspec-test-fixture' do
require 'mock_plugin'
require_relative 'mock_plugin'
InspecPlugins::TestFixture
end
end

View file

@ -4,13 +4,13 @@ module InspecPlugins
class Plugin < Inspec.plugin(2)
plugin_name :'inspec-meaning-of-life'
mock_plugin_type 'meaning-of-life-the-universe-and-everything' do
mock_plugin_type :'meaning-of-life-the-universe-and-everything' do
# NOTE: we can't use require, because these test files are repeatedly reloaded
load 'test/unit/mock/plugins/meaning_of_life_path_mode/inspec-meaning-of-life/mock_plugin.rb'
InspecPlugins::MeaningOfLife::MockPlugin
end
cli_command 'meaningoflife' do
cli_command :'meaningoflife' do
# NOTE: we can't use require, because these test files are repeatedly reloaded
load 'test/unit/mock/plugins/meaning_of_life_path_mode/inspec-meaning-of-life/cli_command.rb'
InspecPlugins::MeaningOfLife::CliCommand

View file

@ -23,8 +23,8 @@ class PluginLoaderTests < MiniTest::Test
@config_dir_path = File.join(mock_path, 'config_dirs')
@bundled_plugins = [
:compliance,
:supermarket,
:'inspec-compliance',
:'inspec-supermarket',
]
@core_plugins = [
:'inspec-artifact',