mirror of
https://github.com/inspec/inspec
synced 2024-11-10 23:24:18 +00:00
Plugins API v2: Loader, Base API, and Test Harness (#3278)
* Functional tests for userdir option * Accepts --config-dir CLI option * Actually loads a config file from the config dir, more cases to test * Able to load config and verify contents from config-dir * Functional tests to ensure precedence for config options * Enable setting config dir via env var * .inspec, not .inspec.d * Begin converting PluginCtl to PluginLoader/Registry * Able to load and partially validate the plugins.json file * More work on the plugin loader * Break the world, move next gen stuff to plugin/ * Be sure to require base cli in bundled plugins * Move test file * Revert changes to v1 plugin, so we can have a separate one * Checkpoint commit * Move v2 plugin work to v2 area * Move plugins v1 code into an isolated directory * rubocop fixes * Rip out the stuff about a user-dir config file, just use a plugin file * Two psuedocode test file * Working base API, moock plugin type, and loader. * Adjust load path to be more welcoming * Silence circular depencency warning, which was breaking a unit test * Linting * Fix plugin type registry, add tests to cover * Feedback from Jerry Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
parent
1bd6ab08e5
commit
811318f2f8
50 changed files with 1158 additions and 36 deletions
161
docs/dev/plugins.md
Normal file
161
docs/dev/plugins.md
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
# Developing InSpec Plugins for the v2 plugin API
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
### Inspiration
|
||||||
|
|
||||||
|
The software design of the InSpec Plugin v2 API is deeply inspired by the Vagrant plugin v2 system. While the InSpec Plugin v2 system is an independent implementation, acknowledgements are due to the Hashicorp team for such a well-thought-out design.
|
||||||
|
|
||||||
|
### Note About versions
|
||||||
|
|
||||||
|
"v2" refers to the second major version of the Plugin API. It doesn't refer to the InSpec release number.
|
||||||
|
|
||||||
|
### Design Goals
|
||||||
|
|
||||||
|
* Load-on-demand. Improve `inspec` startup time by making plugins load heavy libraries only if they are being used.
|
||||||
|
* Independent velocity. Enable passionate community members to contribute at their own pace by shifting core development into plugin development
|
||||||
|
* Increase dogfooding. Convert internal components into plugins to reduce core complexity and allow testing in isolation
|
||||||
|
|
||||||
|
### Design Anti-goals
|
||||||
|
|
||||||
|
* Don't implement resources in plugins; use resource packs for that.
|
||||||
|
|
||||||
|
## How Plugins are Located and Loaded
|
||||||
|
|
||||||
|
### Plugins are usually gems
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### Plugins may also be found by path
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### The plugins.json file
|
||||||
|
|
||||||
|
InSpec stores its list of known plugins in a file, `~/.inspec/plugins.json`. The purpose of this file is avoid having to do a gem path filesystem scan to locate plugins. When you install, update, or uninstall a plugin using `inspec plugin`, InSpec updates this file.
|
||||||
|
|
||||||
|
You can tell inspec to use a different config directory using the INSPEC_CONFIG_DIR environment variable.
|
||||||
|
|
||||||
|
Top-level entries in the JSON file:
|
||||||
|
|
||||||
|
* `plugins_config_version` - must have the value "1.0.0". Reserved for future format changes.
|
||||||
|
* `plugins` - an Array of Hashes, each containing information about plugins that are expected to be installed
|
||||||
|
|
||||||
|
Each plugin entry may have the following keys:
|
||||||
|
|
||||||
|
* `name` - Required. String name of the plugin. Internal machine name of the plugin. Must match `plugin_name` DSL call (see Plugin class below).
|
||||||
|
* `installation_type` - Optional, default "gem". Selects a loading mechanism, may be either "path" or "gem"
|
||||||
|
* `installation_path` - Required if installation_type is "path". A `require` will be attempted against this path. It may be absolute or relative; InSpec adds both the process current working directory as well as the InSpec installation root to the load path.
|
||||||
|
|
||||||
|
TODO: keys for gem installations
|
||||||
|
|
||||||
|
Putting this all together, here is a plugins.json file from the InSpec test suite:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins_config_version" : "1.0.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "inspec-meaning-of-life",
|
||||||
|
"installation_type": "path",
|
||||||
|
"installation_path": "test/unit/mock/plugins/meaning_of_life_path_mode/inspec-meaning-of-life"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Parts
|
||||||
|
|
||||||
|
### A Typical Plugin File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
inspec-my-plugin.gemspec
|
||||||
|
lib/
|
||||||
|
inspec-my-plugin.rb # Entry point
|
||||||
|
inspec-my-plugin/
|
||||||
|
cli.rb # An implementation file
|
||||||
|
plugin.rb # Plugin definition file
|
||||||
|
heavyweight.rb # A support file
|
||||||
|
```
|
||||||
|
|
||||||
|
Generally, except for the entry point, you may name these files anything you like; however, the above example is the typical convention.
|
||||||
|
|
||||||
|
### Gemspec and Plugin Dependencies
|
||||||
|
|
||||||
|
This is a normal Gem specification file. When you release your plugin as a gem, you can declare dependencies here, and InSpec will automatically install them along with your plugin.
|
||||||
|
|
||||||
|
If you are using a path-based install, InSpec will not manage your dependencies.
|
||||||
|
|
||||||
|
### Entry Point
|
||||||
|
|
||||||
|
The entry point is the file that will be `require`d at load time (*not* activation time; see Plugin Lifecycle, below). You should load the bare minimum here - only the plugin definition file. Do not load any plugin dependencies in this file.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# lib/inspec-my-plugin.rb
|
||||||
|
require_relative 'inspec-my-plugin/plugin'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Definition File
|
||||||
|
|
||||||
|
The plugin definition file uses the plugin DSL to declare a small amount of metadata, followed by as many activation hooks as your plugin needs.
|
||||||
|
|
||||||
|
While you may use any valid Ruby module name, we encourage you to namespace your plugin under `InspecPlugins::YOUR_PLUGIN`.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# lib/inspec-my-plugin/plugin.rb
|
||||||
|
module InspecPlugins
|
||||||
|
module MyPlugin
|
||||||
|
# Class name doesn't matter, but this is a reasonable default name
|
||||||
|
class PluginDefinition < Inspec.plugin(2)
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
# Must match entry in plugins.json
|
||||||
|
plugin_name :'inspec-my-plugin'
|
||||||
|
|
||||||
|
# Activation hooks
|
||||||
|
# TODO
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Files
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Plugin Lifecycle
|
||||||
|
|
||||||
|
All queries regarding plugin state should be directed to Inspec::Plugin::V2::Registry.instance, a singleton object.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
registry = Inspec::Plugin::V2::Registry.instance
|
||||||
|
plugin_status = registry[:'inspec-meaning-of-life']
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discovery (Known Plugins)
|
||||||
|
|
||||||
|
If a plugin is mentioned in `plugins.json`, it is *known*. You can get its status, a `Inspec::Plugin::V2::Status` object.
|
||||||
|
|
||||||
|
Reading the plugins.json file is handled by the Loader when Loader.new is called; at that point the registry should know about plugins.
|
||||||
|
|
||||||
|
### Loading
|
||||||
|
|
||||||
|
Next, we load plugins. Loading means that we `require` the entry point determined from the plugins.json. Your plugin definition file will thus execute.
|
||||||
|
|
||||||
|
If things go right, the Status now has a bunch of Activators, each with a block that has not yet executed.
|
||||||
|
|
||||||
|
If things go wrong, have a look at `status.load_exception`.
|
||||||
|
|
||||||
|
### Activation
|
||||||
|
|
||||||
|
Depending on the plugin type, activation may be triggered by a number of different events.
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Execution
|
||||||
|
|
||||||
|
Depending on the plugin type, execution may be triggered by a number of different events.
|
||||||
|
|
||||||
|
TODO
|
|
@ -6,6 +6,7 @@ require 'pathname'
|
||||||
require 'set'
|
require 'set'
|
||||||
require 'tempfile'
|
require 'tempfile'
|
||||||
require 'yaml'
|
require 'yaml'
|
||||||
|
require 'inspec/base_cli'
|
||||||
|
|
||||||
# Notes:
|
# Notes:
|
||||||
#
|
#
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
require 'thor'
|
require 'thor'
|
||||||
require 'erb'
|
require 'erb'
|
||||||
|
require 'inspec/base_cli'
|
||||||
|
|
||||||
module Compliance
|
module Compliance
|
||||||
class ComplianceCLI < Inspec::BaseCLI
|
class ComplianceCLI < Inspec::BaseCLI
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# author: Adam Leff
|
# author: Adam Leff
|
||||||
|
|
||||||
require 'thor'
|
require 'thor'
|
||||||
|
require 'inspec/base_cli'
|
||||||
|
|
||||||
module Habitat
|
module Habitat
|
||||||
class HabitatProfileCLI < Thor
|
class HabitatProfileCLI < Thor
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
require 'pathname'
|
require 'pathname'
|
||||||
require_relative 'renderer'
|
require_relative 'renderer'
|
||||||
|
require 'inspec/base_cli'
|
||||||
|
|
||||||
module Init
|
module Init
|
||||||
class CLI < Inspec::BaseCLI
|
class CLI < Inspec::BaseCLI
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
|
require 'inspec/base_cli'
|
||||||
|
|
||||||
module Supermarket
|
module Supermarket
|
||||||
class SupermarketCLI < Inspec::BaseCLI
|
class SupermarketCLI < Inspec::BaseCLI
|
||||||
|
|
|
@ -16,9 +16,11 @@ require 'inspec/shell'
|
||||||
require 'inspec/formatters'
|
require 'inspec/formatters'
|
||||||
require 'inspec/reporters'
|
require 'inspec/reporters'
|
||||||
|
|
||||||
# all utils that may be required by plugins
|
require 'inspec/plugin/v2'
|
||||||
|
require 'inspec/plugin/v1'
|
||||||
|
|
||||||
|
# all utils that may be required by legacy plugins
|
||||||
require 'inspec/base_cli'
|
require 'inspec/base_cli'
|
||||||
require 'inspec/fetcher'
|
require 'inspec/fetcher'
|
||||||
require 'inspec/source_reader'
|
require 'inspec/source_reader'
|
||||||
require 'inspec/resource'
|
require 'inspec/resource'
|
||||||
require 'inspec/plugins'
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ require 'pp'
|
||||||
require 'utils/json_log'
|
require 'utils/json_log'
|
||||||
require 'utils/latest_version'
|
require 'utils/latest_version'
|
||||||
require 'inspec/base_cli'
|
require 'inspec/base_cli'
|
||||||
require 'inspec/plugins'
|
require 'inspec/plugin/v1'
|
||||||
|
require 'inspec/plugin/v2'
|
||||||
require 'inspec/runner_mock'
|
require 'inspec/runner_mock'
|
||||||
require 'inspec/env_printer'
|
require 'inspec/env_printer'
|
||||||
require 'inspec/schema'
|
require 'inspec/schema'
|
||||||
|
@ -20,7 +21,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
||||||
desc: 'Set the log level: info (default), debug, warn, error'
|
desc: 'Set the log level: info (default), debug, warn, error'
|
||||||
|
|
||||||
class_option :log_location, type: :string,
|
class_option :log_location, type: :string,
|
||||||
desc: 'Location to send diagnostic log messages to. (default: STDOUT or STDERR)'
|
desc: 'Location to send diagnostic log messages to. (default: STDOUT or Inspec::Log.error)'
|
||||||
|
|
||||||
class_option :diagnose, type: :boolean,
|
class_option :diagnose, type: :boolean,
|
||||||
desc: 'Show diagnostics (versions, configurations)'
|
desc: 'Show diagnostics (versions, configurations)'
|
||||||
|
@ -282,17 +283,34 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load all plugins on startup
|
begin
|
||||||
ctl = Inspec::PluginCtl.new
|
# Load v2 plugins
|
||||||
ctl.list.each { |x| ctl.load(x) }
|
v2_loader = Inspec::Plugin::V2::Loader.new
|
||||||
|
v2_loader.load_all
|
||||||
|
v2_loader.exit_on_load_error
|
||||||
|
|
||||||
# load CLI plugins before the Inspec CLI has been started
|
# Load v1 plugins on startup
|
||||||
Inspec::Plugins::CLI.subcommands.each { |_subcommand, params|
|
ctl = Inspec::PluginCtl.new
|
||||||
Inspec::InspecCLI.register(
|
ctl.list.each { |x| ctl.load(x) }
|
||||||
params[:klass],
|
|
||||||
params[:subcommand_name],
|
# load v1 CLI plugins before the Inspec CLI has been started
|
||||||
params[:usage],
|
Inspec::Plugins::CLI.subcommands.each { |_subcommand, params|
|
||||||
params[:description],
|
Inspec::InspecCLI.register(
|
||||||
params[:options],
|
params[:klass],
|
||||||
)
|
params[:subcommand_name],
|
||||||
}
|
params[:usage],
|
||||||
|
params[:description],
|
||||||
|
params[:options],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rescue Inspec::Plugin::V2::Exception => v2ex
|
||||||
|
Inspec::Log.error v2ex.message
|
||||||
|
|
||||||
|
if ARGV.include?('--debug')
|
||||||
|
Inspec::Log.error v2ex.class.name
|
||||||
|
Inspec::Log.error v2ex.backtrace.join("\n")
|
||||||
|
else
|
||||||
|
Inspec::Log.error 'Run again with --debug for a stacktrace.'
|
||||||
|
end
|
||||||
|
exit 2
|
||||||
|
end
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
require 'inspec/cached_fetcher'
|
require 'inspec/cached_fetcher'
|
||||||
require 'inspec/dependencies/dependency_set'
|
|
||||||
require 'semverse'
|
require 'semverse'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
|
|
||||||
require 'inspec/plugins'
|
require 'inspec/plugin/v1'
|
||||||
require 'utils/plugin_registry'
|
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
class FetcherRegistry < PluginRegistry
|
class FetcherRegistry < PluginRegistry
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
# author: Steven Danna
|
# author: Steven Danna
|
||||||
# author: Victoria Jeffrey
|
# author: Victoria Jeffrey
|
||||||
require 'inspec/plugins/resource'
|
require 'inspec/plugin/v1/plugin_types/resource'
|
||||||
require 'inspec/dsl_shared'
|
require 'inspec/dsl_shared'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
|
|
2
lib/inspec/plugin/v1.rb
Normal file
2
lib/inspec/plugin/v1.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
require 'inspec/plugin/v1/plugins'
|
||||||
|
require 'inspec/plugin/v1/registry'
|
|
@ -2,6 +2,8 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
|
|
||||||
|
require 'inspec/plugin/v1/registry'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
module Plugins
|
module Plugins
|
||||||
# stores all CLI plugin, we expect those to the `Thor` subclasses
|
# stores all CLI plugin, we expect those to the `Thor` subclasses
|
|
@ -1,8 +1,8 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
require 'utils/plugin_registry'
|
|
||||||
require 'inspec/file_provider'
|
require 'inspec/file_provider'
|
||||||
|
require 'inspec/plugin/v1/registry'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
module Plugins
|
module Plugins
|
|
@ -2,7 +2,7 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
|
|
||||||
require 'utils/plugin_registry'
|
require 'inspec/plugin/v1/registry'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
module Plugins
|
module Plugins
|
|
@ -2,7 +2,7 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
|
|
||||||
require 'utils/plugin_registry'
|
require 'inspec/plugin/v1/registry'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
module Plugins
|
module Plugins
|
|
@ -6,12 +6,14 @@ require 'forwardable'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
# Resource Plugins
|
# Resource Plugins
|
||||||
|
# NOTE: the autoloading here is rendered moot by the fact that
|
||||||
|
# all core plugins are `require`'d by the base inspec.rb
|
||||||
module Plugins
|
module Plugins
|
||||||
autoload :Resource, 'inspec/plugins/resource'
|
autoload :Resource, 'inspec/plugin/v1/plugin_types/resource'
|
||||||
autoload :CLI, 'inspec/plugins/cli'
|
autoload :CLI, 'inspec/plugin/v1/plugin_types/cli'
|
||||||
autoload :Fetcher, 'inspec/plugins/fetcher'
|
autoload :Fetcher, 'inspec/plugin/v1/plugin_types/fetcher'
|
||||||
autoload :SourceReader, 'inspec/plugins/source_reader'
|
autoload :SourceReader, 'inspec/plugin/v1/plugin_types/source_reader'
|
||||||
autoload :Secret, 'inspec/plugins/secret'
|
autoload :Secret, 'inspec/plugin/v1/plugin_types/secret'
|
||||||
end
|
end
|
||||||
|
|
||||||
# PLEASE NOTE: The Plugin system is an internal mechanism for connecting
|
# PLEASE NOTE: The Plugin system is an internal mechanism for connecting
|
30
lib/inspec/plugin/v2.rb
Normal file
30
lib/inspec/plugin/v2.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
require 'inspec/errors'
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
module Plugin
|
||||||
|
module V2
|
||||||
|
class Exception < Inspec::Error; end
|
||||||
|
class ConfigError < Inspec::Plugin::V2::Exception; end
|
||||||
|
class LoadError < Inspec::Plugin::V2::Exception; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require_relative 'v2/registry'
|
||||||
|
require_relative 'v2/loader'
|
||||||
|
require_relative 'v2/plugin_base'
|
||||||
|
|
||||||
|
# Load all plugin type base classes
|
||||||
|
Dir.glob(File.join(__dir__, 'v2', 'plugin_types', '*.rb')).each { |file| require file }
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
# Provides the base class that plugin implementors should use.
|
||||||
|
def self.plugin(version, plugin_type = nil)
|
||||||
|
unless version == 2
|
||||||
|
raise 'Only plugins version 2 is supported!'
|
||||||
|
end
|
||||||
|
|
||||||
|
return Inspec::Plugin::V2::PluginBase if plugin_type.nil?
|
||||||
|
Inspec::Plugin::V2::PluginBase.base_class_for_type(plugin_type)
|
||||||
|
end
|
||||||
|
end
|
16
lib/inspec/plugin/v2/activator.rb
Normal file
16
lib/inspec/plugin/v2/activator.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module Inspec::Plugin::V2
|
||||||
|
Activator = Struct.new(
|
||||||
|
:plugin_name,
|
||||||
|
:plugin_type,
|
||||||
|
:activator_name,
|
||||||
|
:activated,
|
||||||
|
:exception,
|
||||||
|
:activation_proc,
|
||||||
|
:implementation_class,
|
||||||
|
) do
|
||||||
|
def initialize(*)
|
||||||
|
super
|
||||||
|
self[:activated] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
191
lib/inspec/plugin/v2/loader.rb
Normal file
191
lib/inspec/plugin/v2/loader.rb
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
require 'json'
|
||||||
|
require 'inspec/log'
|
||||||
|
|
||||||
|
# Add the current directory of the process to the load path
|
||||||
|
$LOAD_PATH.unshift('.') unless $LOAD_PATH.include?('.')
|
||||||
|
# Add the InSpec source root directory to the load path
|
||||||
|
folder = File.expand_path(File.join('..', '..', '..', '..'), __dir__)
|
||||||
|
$LOAD_PATH.unshift(folder) unless $LOAD_PATH.include?('folder')
|
||||||
|
|
||||||
|
module Inspec::Plugin::V2
|
||||||
|
class Loader
|
||||||
|
attr_reader :registry, :options
|
||||||
|
|
||||||
|
def initialize(options = {})
|
||||||
|
@options = options
|
||||||
|
@registry = Inspec::Plugin::V2::Registry.instance
|
||||||
|
determine_plugin_conf_file
|
||||||
|
read_conf_file
|
||||||
|
unpack_conf_file
|
||||||
|
detect_bundled_plugins unless options[:omit_bundles]
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_all
|
||||||
|
registry.each do |plugin_name, plugin_details|
|
||||||
|
# We want to capture literally any possible exception here, since we are storing them.
|
||||||
|
# rubocop: disable Lint/RescueException
|
||||||
|
begin
|
||||||
|
# We could use require, but under testing, we need to repeatedly reload the same
|
||||||
|
# plugin.
|
||||||
|
if plugin_details.entry_point.include?('test/unit/mock/plugins')
|
||||||
|
load plugin_details.entry_point + '.rb'
|
||||||
|
else
|
||||||
|
require plugin_details.entry_point
|
||||||
|
end
|
||||||
|
plugin_details.loaded = true
|
||||||
|
annotate_status_after_loading(plugin_name)
|
||||||
|
rescue ::Exception => ex
|
||||||
|
plugin_details.load_exception = ex
|
||||||
|
Inspec::Log.error "Could not load plugin #{plugin_name}"
|
||||||
|
end
|
||||||
|
# rubocop: enable Lint/RescueException
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This should possibly be in either lib/inspec/cli.rb or Registry
|
||||||
|
def exit_on_load_error
|
||||||
|
if registry.any_load_failures?
|
||||||
|
Inspec::Log.error 'Errors were encountered while loading plugins...'
|
||||||
|
registry.plugin_statuses.select(&:load_exception).each do |plugin_status|
|
||||||
|
Inspec::Log.error 'Plugin name: ' + plugin_status.name.to_s
|
||||||
|
Inspec::Log.error 'Error: ' + plugin_status.load_exception.message
|
||||||
|
if ARGV.include?('--debug')
|
||||||
|
Inspec::Log.error 'Exception: ' + plugin_status.load_exception.class.name
|
||||||
|
Inspec::Log.error 'Trace: ' + plugin_status.load_exception.backtrace.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Inspec::Log.error('Run again with --debug for a stacktrace.') unless ARGV.include?('--debug')
|
||||||
|
exit 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate(plugin_type, hook_name)
|
||||||
|
activator = registry.find_activators(plugin_type: plugin_type, activation_name: hook_name).first
|
||||||
|
# We want to capture literally any possible exception here, since we are storing them.
|
||||||
|
# rubocop: disable Lint/RescueException
|
||||||
|
begin
|
||||||
|
impl_class = activator.activation_proc.call
|
||||||
|
activator.activated = true
|
||||||
|
activator.implementation_class = impl_class
|
||||||
|
rescue Exception => ex
|
||||||
|
activator.exception = ex
|
||||||
|
Inspec::Log.error "Could not activate #{activator.plugin_type} hook named '#{activator.activator_name}' for plugin #{plugin_name}"
|
||||||
|
end
|
||||||
|
# rubocop: enable Lint/RescueException
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def annotate_status_after_loading(plugin_name)
|
||||||
|
status = registry[plugin_name]
|
||||||
|
return if status.api_generation == 2 # Gen2 have self-annotating superclasses
|
||||||
|
case status.installation_type
|
||||||
|
when :bundle
|
||||||
|
annotate_bundle_plugin_status_after_load(plugin_name)
|
||||||
|
else
|
||||||
|
# TODO: are there any other cases? can this whole thing be eliminated?
|
||||||
|
raise "I only know how to annotate :bundle plugins when trying to load plugin #{plugin_name}" unless status.installation_type == :bundle
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def annotate_bundle_plugin_status_after_load(plugin_name)
|
||||||
|
# HACK: we're relying on the fact that all bundles are gen0 and cli type
|
||||||
|
status = registry[plugin_name]
|
||||||
|
status.api_generation = 0
|
||||||
|
act = Activator.new
|
||||||
|
act.activated = true
|
||||||
|
act.plugin_type = :cli_command
|
||||||
|
act.plugin_name = plugin_name
|
||||||
|
act.activator_name = :default
|
||||||
|
status.activators = [act]
|
||||||
|
|
||||||
|
v0_subcommand_name = plugin_name.to_s.gsub('inspec-', '')
|
||||||
|
status.plugin_class = Inspec::Plugins::CLI.subcommands[v0_subcommand_name][:klass]
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_bundled_plugins
|
||||||
|
bundle_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'bundles'))
|
||||||
|
globs = [
|
||||||
|
File.join(bundle_dir, 'inspec-*.rb'),
|
||||||
|
File.join(bundle_dir, 'train-*.rb'),
|
||||||
|
]
|
||||||
|
Dir.glob(globs).each do |loader_file|
|
||||||
|
name = File.basename(loader_file, '.rb').gsub(/^(inspec|train)-/, '')
|
||||||
|
status = Inspec::Plugin::V2::Status.new
|
||||||
|
status.name = name
|
||||||
|
status.entry_point = loader_file
|
||||||
|
status.installation_type = :bundle
|
||||||
|
status.loaded = false
|
||||||
|
registry[name] = status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def determine_plugin_conf_file
|
||||||
|
@plugin_conf_file_path = ENV['INSPEC_CONFIG_DIR'] ? ENV['INSPEC_CONFIG_DIR'] : File.join(Dir.home, '.inspec')
|
||||||
|
@plugin_conf_file_path = File.join(@plugin_conf_file_path, 'plugins.json')
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_conf_file
|
||||||
|
if File.exist?(@plugin_conf_file_path)
|
||||||
|
@plugin_file_contents = JSON.parse(File.read(@plugin_conf_file_path))
|
||||||
|
else
|
||||||
|
@plugin_file_contents = {
|
||||||
|
'plugins_config_version' => '1.0.0',
|
||||||
|
'plugins' => [],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
rescue JSON::ParserError => e
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Failed to load plugins JSON configuration from #{@plugin_conf_file_path}:\n#{e}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def unpack_conf_file
|
||||||
|
validate_conf_file
|
||||||
|
@plugin_file_contents['plugins'].each do |plugin_json|
|
||||||
|
status = Inspec::Plugin::V2::Status.new
|
||||||
|
status.name = plugin_json['name'].to_sym
|
||||||
|
status.loaded = false
|
||||||
|
status.installation_type = plugin_json['installation_type'].to_sym || :gem
|
||||||
|
case status.installation_type
|
||||||
|
when :gem
|
||||||
|
status.entry_point = status.name
|
||||||
|
status.version = plugin_json['version']
|
||||||
|
when :path
|
||||||
|
status.entry_point = plugin_json['installation_path']
|
||||||
|
end
|
||||||
|
|
||||||
|
registry[status.name] = status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_conf_file
|
||||||
|
unless @plugin_file_contents['plugins_config_version'] == '1.0.0'
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Unsupported plugins.json file version #{@plugin_file_contents['plugins_config_version']} at #{@plugin_conf_file_path} - currently support versions: 1.0.0"
|
||||||
|
end
|
||||||
|
|
||||||
|
plugin_entries = @plugin_file_contents['plugins']
|
||||||
|
unless plugin_entries.is_a?(Array)
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - should have a top-level key named 'plugins', whose value is an array"
|
||||||
|
end
|
||||||
|
|
||||||
|
plugin_entries.each do |plugin_entry|
|
||||||
|
unless plugin_entry.is_a? Hash
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry should be a Hash / JSON object"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless plugin_entry.key? 'name'
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry must have a 'name' field"
|
||||||
|
end
|
||||||
|
|
||||||
|
next unless plugin_entry.key?('installation_type')
|
||||||
|
unless %w{gem path}.include? plugin_entry['installation_type']
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'installation_type' must be one of 'gem' or 'path'"
|
||||||
|
end
|
||||||
|
|
||||||
|
next unless plugin_entry['installation_type'] == 'path'
|
||||||
|
unless plugin_entry.key?('installation_path')
|
||||||
|
raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry with a 'path' installation_type must provide an 'installation_path' field"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
98
lib/inspec/plugin/v2/plugin_base.rb
Normal file
98
lib/inspec/plugin/v2/plugin_base.rb
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
module Inspec::Plugin::V2
|
||||||
|
# Base class for all plugins. Specific plugin types *may* inherit from this; but they must register with it.
|
||||||
|
class PluginBase
|
||||||
|
# rubocop: disable Style/ClassVars
|
||||||
|
@@plugin_type_classes = {}
|
||||||
|
# rubocop: enable Style/ClassVars
|
||||||
|
|
||||||
|
#=====================================================================#
|
||||||
|
# Management Methods
|
||||||
|
#=====================================================================#
|
||||||
|
|
||||||
|
# The global registry.
|
||||||
|
# @returns [Inspec::Plugin::V2::Registry] the singleton Plugin Registry object.
|
||||||
|
def self.registry
|
||||||
|
Inspec::Plugin::V2::Registry.instance
|
||||||
|
end
|
||||||
|
|
||||||
|
# Inform the plugin system of a new plgin type.
|
||||||
|
# This has the following effects:
|
||||||
|
# * enables Inspec.plugin(2, :your_type_here) to return the plugin
|
||||||
|
# type base class
|
||||||
|
# * defines the DSL method with the same name as the plugin type.
|
||||||
|
#
|
||||||
|
# @ param [Symbol] plugin_type_name
|
||||||
|
def self.register_plugin_type(plugin_type_name)
|
||||||
|
new_dsl_method_name = plugin_type_name
|
||||||
|
new_plugin_type_base_class = self
|
||||||
|
|
||||||
|
# This lets the Inspec.plugin(2,:your_plugin) work
|
||||||
|
@@plugin_type_classes[plugin_type_name] = new_plugin_type_base_class
|
||||||
|
|
||||||
|
# This part defines the DSL command to register a concrete plugin's implementation of a plugin type
|
||||||
|
Inspec::Plugin::V2::PluginBase.define_singleton_method(new_dsl_method_name) do |hook_name, &hook_body|
|
||||||
|
plugin_concrete_class = self
|
||||||
|
|
||||||
|
# Verify class is registered (i.e. plugin_name has been called)
|
||||||
|
status = registry.find_status_by_class(plugin_concrete_class)
|
||||||
|
if status.nil?
|
||||||
|
raise Inspec::Plugin::V2::LoadError, "You must call 'plugin_name' prior to calling #{plugin_type_name} for plugin class #{plugin_concrete_class}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Construct the Activator record
|
||||||
|
activator = Inspec::Plugin::V2::Activator.new
|
||||||
|
activator.plugin_name = plugin_concrete_class.plugin_name
|
||||||
|
activator.plugin_type = plugin_type_name
|
||||||
|
activator.activator_name = hook_name.to_sym
|
||||||
|
activator.activation_proc = hook_body
|
||||||
|
|
||||||
|
status.activators << activator
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determine the base class for a given plugin type
|
||||||
|
# @param [Symbol] plugin_type_name
|
||||||
|
# @returns [Class] the plugin type base class
|
||||||
|
def self.base_class_for_type(plugin_type_name)
|
||||||
|
@@plugin_type_classes[plugin_type_name]
|
||||||
|
end
|
||||||
|
|
||||||
|
#=====================================================================#
|
||||||
|
# DSL Methods
|
||||||
|
#=====================================================================#
|
||||||
|
|
||||||
|
# If no name provided, looks up a known plugin by class and returns the name.
|
||||||
|
#
|
||||||
|
# DSL method to declare a plugin. Once this has been called, the plugin will certainly be
|
||||||
|
# registered (known) with the Registry, and is eligible to be activated.
|
||||||
|
# This mainly handles status annotation.
|
||||||
|
#
|
||||||
|
# @param [Symbol] Name of the plugin. If a string is provided, it is converted to a Symbol.
|
||||||
|
# @returns [Symbol] Name of the plugin
|
||||||
|
def self.plugin_name(name = nil)
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
return reg.find_status_by_class(self).name if name.nil?
|
||||||
|
|
||||||
|
name = name.to_sym
|
||||||
|
|
||||||
|
# Typically our status will already exist in the registry, from loading the
|
||||||
|
# plugin.json. If we're being loaded, presumably entry_point,
|
||||||
|
# installation_type, version
|
||||||
|
# are known.
|
||||||
|
unless reg.known_plugin?(name)
|
||||||
|
# Under some testing situations, we may not pre-exist.
|
||||||
|
status = Inspec::Plugin::V2::Status.new
|
||||||
|
reg.register(name, status)
|
||||||
|
status.entry_point = 'inline'
|
||||||
|
status.installation_type = :mock_inline
|
||||||
|
end
|
||||||
|
|
||||||
|
status = reg[name]
|
||||||
|
status.api_generation = 2
|
||||||
|
status.plugin_class = self
|
||||||
|
status.name = name
|
||||||
|
|
||||||
|
name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
0
lib/inspec/plugin/v2/plugin_types/cli.rb
Normal file
0
lib/inspec/plugin/v2/plugin_types/cli.rb
Normal file
12
lib/inspec/plugin/v2/plugin_types/mock.rb
Normal file
12
lib/inspec/plugin/v2/plugin_types/mock.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module Inspec::Plugin::V2::PluginType
|
||||||
|
# Test plugin type
|
||||||
|
class Mock < Inspec::Plugin::V2::PluginBase
|
||||||
|
register_plugin_type(:mock_plugin_type)
|
||||||
|
|
||||||
|
# This is the API for the mock plugin type: when a mock plugin is
|
||||||
|
# activated, it is expected to be able to respond to this, and "do something"
|
||||||
|
def mock_hook
|
||||||
|
raise NotImplementedError, 'Mock plugins must implement mock_hook'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
75
lib/inspec/plugin/v2/registry.rb
Normal file
75
lib/inspec/plugin/v2/registry.rb
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
require 'forwardable'
|
||||||
|
require 'singleton'
|
||||||
|
require_relative 'status'
|
||||||
|
require_relative 'activator'
|
||||||
|
|
||||||
|
module Inspec::Plugin::V2
|
||||||
|
class Registry
|
||||||
|
include Singleton
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
attr_reader :registry
|
||||||
|
def_delegator :registry, :each
|
||||||
|
def_delegator :registry, :[]
|
||||||
|
def_delegator :registry, :key?, :known_plugin?
|
||||||
|
def_delegator :registry, :keys, :plugin_names
|
||||||
|
def_delegator :registry, :values, :plugin_statuses
|
||||||
|
def_delegator :registry, :select
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@registry = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def any_load_failures?
|
||||||
|
!plugin_statuses.select(&:load_exception).empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def loaded_plugin?(name)
|
||||||
|
registry.dig(name, :loaded)
|
||||||
|
end
|
||||||
|
|
||||||
|
def loaded_count
|
||||||
|
registry.values.select(&:loaded).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def known_count
|
||||||
|
registry.values.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def loaded_plugin_names
|
||||||
|
registry.values.select(&:loaded).map(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_status_by_class(klass)
|
||||||
|
registry.values.detect { |status| status.plugin_class == klass }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Finds Activators matching criteria (all optional) you specify as a Hash.
|
||||||
|
# @param [Symbol] plugin_name Restricts the search to the given plugin
|
||||||
|
# @param [Symbol] plugin_type Restricts the search to the given plugin type
|
||||||
|
# @param [Symbol] activator_name Name of the activator
|
||||||
|
# @returns [Array] Possibly empty array of Activators
|
||||||
|
def find_activators(filters = {})
|
||||||
|
plugin_statuses.map(&:activators).flatten.select do |act|
|
||||||
|
[:plugin_name, :plugin_type, :activator_name].all? do |criteria|
|
||||||
|
!filters.key?(criteria) || act[criteria] == filters[criteria]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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}"
|
||||||
|
else
|
||||||
|
registry[name.to_sym] = status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alias []= register
|
||||||
|
|
||||||
|
# Provided for test support. Purges the registry.
|
||||||
|
def __reset
|
||||||
|
@registry.clear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
lib/inspec/plugin/v2/status.rb
Normal file
29
lib/inspec/plugin/v2/status.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module Inspec::Plugin::V2
|
||||||
|
# Track loading status of each plugin. These are the elements of the Registry.
|
||||||
|
#
|
||||||
|
# Lifecycle of an installed plugin:
|
||||||
|
# If present in the config file, bundled, or core, it is "known"
|
||||||
|
# All known plugins are loaded. v1 plugins auto-activate. All loaded plugins know their version.
|
||||||
|
# v2 plugins activate when they are used. All activated plugins know their implementation class.
|
||||||
|
Status = Struct.new(
|
||||||
|
:activators, # Array of Activators - where plugin_type info gets stored
|
||||||
|
:api_generation, # 0,1,2 # TODO: convert all bundled (v0) to v2
|
||||||
|
:plugin_class, # Plugin class
|
||||||
|
:entry_point, # a gem name or filesystem path
|
||||||
|
:installation_type, # :gem, :path, :core, bundle # TODO: combine core and bundle
|
||||||
|
:loaded, # true, false False could mean not attempted or failed
|
||||||
|
:load_exception, # Exception class if it failed to load
|
||||||
|
:name, # String name
|
||||||
|
:version, # three-digit version. Core / bundled plugins use InSpec version here.
|
||||||
|
) do
|
||||||
|
def initialize(*)
|
||||||
|
super
|
||||||
|
self[:activators] = []
|
||||||
|
self[:loaded] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def plugin_types
|
||||||
|
activators.map(&:plugin_type).uniq.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
# copyright: 2015, Vulcano Security GmbH
|
# copyright: 2015, Vulcano Security GmbH
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
require 'inspec/plugins'
|
require 'inspec/plugin/v1'
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
class ProfileNotFound < StandardError; end
|
class ProfileNotFound < StandardError; end
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
|
|
||||||
require 'inspec/plugins'
|
require 'inspec/plugin/v1'
|
||||||
require 'utils/plugin_registry'
|
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
SecretsBackend = PluginRegistry.new
|
SecretsBackend = PluginRegistry.new
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
|
|
||||||
require 'inspec/plugins'
|
require 'inspec/plugin/v1'
|
||||||
require 'utils/plugin_registry'
|
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
# Pre-checking of target resolution. Make sure that SourceReader plugins
|
# Pre-checking of target resolution. Make sure that SourceReader plugins
|
||||||
|
|
|
@ -16,7 +16,8 @@ end
|
||||||
module FunctionalHelper
|
module FunctionalHelper
|
||||||
let(:repo_path) { File.expand_path(File.join( __FILE__, '..', '..', '..')) }
|
let(:repo_path) { File.expand_path(File.join( __FILE__, '..', '..', '..')) }
|
||||||
let(:exec_inspec) { File.join(repo_path, 'bin', 'inspec') }
|
let(:exec_inspec) { File.join(repo_path, 'bin', 'inspec') }
|
||||||
let(:profile_path) { File.join(repo_path, 'test', 'unit', 'mock', 'profiles') }
|
let(:mock_path) { File.join(repo_path, 'test', 'unit', 'mock') }
|
||||||
|
let(:profile_path) { File.join(mock_path, 'profiles') }
|
||||||
let(:examples_path) { File.join(repo_path, 'examples') }
|
let(:examples_path) { File.join(repo_path, 'examples') }
|
||||||
let(:integration_test_path) { File.join(repo_path, 'test', 'integration', 'default') }
|
let(:integration_test_path) { File.join(repo_path, 'test', 'integration', 'default') }
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ module FunctionalHelper
|
||||||
let(:failure_control) { File.join(profile_path, 'failures', 'controls', 'failures.rb') }
|
let(:failure_control) { File.join(profile_path, 'failures', 'controls', 'failures.rb') }
|
||||||
let(:simple_inheritance) { File.join(profile_path, 'simple-inheritance') }
|
let(:simple_inheritance) { File.join(profile_path, 'simple-inheritance') }
|
||||||
let(:sensitive_profile) { File.join(examples_path, 'profile-sensitive') }
|
let(:sensitive_profile) { File.join(examples_path, 'profile-sensitive') }
|
||||||
|
let(:config_dir_path) { File.join(mock_path, 'config_dirs') }
|
||||||
|
|
||||||
let(:dst) {
|
let(:dst) {
|
||||||
# create a temporary path, but we only want an auto-clean helper
|
# create a temporary path, but we only want an auto-clean helper
|
||||||
|
@ -39,6 +41,15 @@ module FunctionalHelper
|
||||||
CMD.run_command("#{prefix} #{exec_inspec} #{commandline}")
|
CMD.run_command("#{prefix} #{exec_inspec} #{commandline}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inspec_with_env(commandline, env = {})
|
||||||
|
# CMD is a train transport, and does not support anything other than a
|
||||||
|
# single param for the command line.
|
||||||
|
# TODO: what is the intent of using Train here?
|
||||||
|
# HACK: glue together env vars
|
||||||
|
env_prefix = env.to_a.map { |assignment| "#{assignment[0]}=#{assignment[1]}" }.join(' ')
|
||||||
|
CMD.run_command("#{env_prefix} #{exec_inspec} #{commandline}")
|
||||||
|
end
|
||||||
|
|
||||||
# Copy all examples to a temporary directory for functional tests.
|
# Copy all examples to a temporary directory for functional tests.
|
||||||
# You can provide an optional directory which will be handed to your
|
# You can provide an optional directory which will be handed to your
|
||||||
# test block with its absolute path. If nothing is provided you will
|
# test block with its absolute path. If nothing is provided you will
|
||||||
|
|
49
test/functional/plugins_test.rb
Normal file
49
test/functional/plugins_test.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# Functional tests related to plugin facility
|
||||||
|
require 'functional/helper'
|
||||||
|
|
||||||
|
#=========================================================================================#
|
||||||
|
# Loader Errors
|
||||||
|
#=========================================================================================#
|
||||||
|
describe 'plugin loader' do
|
||||||
|
include FunctionalHelper
|
||||||
|
|
||||||
|
it 'handles a corrupt plugins.json correctly' do
|
||||||
|
outcome = inspec_with_env('version', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'corrupt'))
|
||||||
|
outcome.exit_status.must_equal 2
|
||||||
|
outcome.stdout.wont_include('Inspec::Plugin::V2::ConfigError', 'No stacktrace in error by default')
|
||||||
|
outcome.stdout.must_include('Failed to load plugins JSON configuration', 'Friendly message in error')
|
||||||
|
outcome.stdout.must_include('unit/mock/config_dirs/corrupt/plugins.json', 'Location of bad file in error')
|
||||||
|
|
||||||
|
outcome = inspec_with_env('version --debug', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'corrupt'))
|
||||||
|
outcome.exit_status.must_equal 2
|
||||||
|
outcome.stdout.must_include('Inspec::Plugin::V2::ConfigError', 'Include stacktrace in error with --debug')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles a misversioned plugins.json correctly' do
|
||||||
|
outcome = inspec_with_env('version', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'bad_plugin_conf_version'))
|
||||||
|
outcome.exit_status.must_equal 2
|
||||||
|
outcome.stdout.wont_include('Inspec::Plugin::V2::ConfigError', 'No stacktrace in error by default')
|
||||||
|
outcome.stdout.must_include('Unsupported plugins.json file version', 'Friendly message in error')
|
||||||
|
outcome.stdout.must_include('unit/mock/config_dirs/bad_plugin_conf_version/plugins.json', 'Location of bad file in error')
|
||||||
|
outcome.stdout.must_include('99.99.9', 'Incorrect version in error')
|
||||||
|
|
||||||
|
outcome = inspec_with_env('version --debug', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'bad_plugin_conf_version'))
|
||||||
|
outcome.exit_status.must_equal 2
|
||||||
|
outcome.stdout.must_include('Inspec::Plugin::V2::ConfigError', 'Include stacktrace in error with --debug')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles an unloadable plugin correctly' do
|
||||||
|
outcome = inspec_with_env('version', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'plugin_error_on_load'))
|
||||||
|
outcome.exit_status.must_equal 2
|
||||||
|
outcome.stdout.must_include('ERROR', 'Have an error on stdout')
|
||||||
|
outcome.stdout.must_include('Could not load plugin inspec-divide-by-zero', 'Name the plugin in the stdout error')
|
||||||
|
outcome.stdout.wont_include('ZeroDivisionError', 'No stacktrace in error by default')
|
||||||
|
outcome.stdout.must_include('Errors were encountered while loading plugins', 'Friendly message in error')
|
||||||
|
outcome.stdout.must_include('Plugin name: inspec-divide-by-zero', 'Plugin named in error')
|
||||||
|
outcome.stdout.must_include('divided by 0', 'Exception message in error')
|
||||||
|
|
||||||
|
outcome = inspec_with_env('version --debug', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'plugin_error_on_load'))
|
||||||
|
outcome.exit_status.must_equal 2
|
||||||
|
outcome.stdout.must_include('ZeroDivisionError', 'Include stacktrace in error with --debug')
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"plugins_config_version" : "99.99.9",
|
||||||
|
"plugins": []
|
||||||
|
}
|
2
test/unit/mock/config_dirs/corrupt/plugins.json
Normal file
2
test/unit/mock/config_dirs/corrupt/plugins.json
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{
|
||||||
|
"plugins_config_version" :
|
0
test/unit/mock/config_dirs/empty/.gitkeep
Normal file
0
test/unit/mock/config_dirs/empty/.gitkeep
Normal file
22
test/unit/mock/config_dirs/fakehome/.inspec/plugins.json
Normal file
22
test/unit/mock/config_dirs/fakehome/.inspec/plugins.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"plugins_config_version" : "1.0.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "inspec-test-plugin-gem",
|
||||||
|
"installation_type": "gem",
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inspec-test-plugin-path",
|
||||||
|
"installation_type": "path",
|
||||||
|
"installation_path": "test/unit/mock/plugins/inspec-test-plugin-path",
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inspec-test-home-marker",
|
||||||
|
"installation_type": "path",
|
||||||
|
"installation_path": "test/unit/mock/plugins/inspec-test-home-marker",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
test/unit/mock/config_dirs/meaning_by_path/plugins.json
Normal file
10
test/unit/mock/config_dirs/meaning_by_path/plugins.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"plugins_config_version" : "1.0.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "inspec-meaning-of-life",
|
||||||
|
"installation_type": "path",
|
||||||
|
"installation_path": "test/unit/mock/plugins/meaning_of_life_path_mode/inspec-meaning-of-life"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
test/unit/mock/config_dirs/plugin_error_on_load/plugins.json
Normal file
10
test/unit/mock/config_dirs/plugin_error_on_load/plugins.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"plugins_config_version" : "1.0.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "inspec-divide-by-zero",
|
||||||
|
"installation_type": "path",
|
||||||
|
"installation_path": "test/unit/mock/plugins/inspec-divide-by-zero/inspec-divide-by-zero"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
1/0
|
|
@ -0,0 +1 @@
|
||||||
|
require 'inspec-meaning-of-life-cli/plugin'
|
|
@ -0,0 +1,13 @@
|
||||||
|
module InspecPlugins
|
||||||
|
module MeaningOfLife
|
||||||
|
class Cli < Inspec.plugin(2, :cli)
|
||||||
|
|
||||||
|
# Do cli-ish things
|
||||||
|
def execute(opts)
|
||||||
|
puts 'The answer to life, the universe, and everything:'
|
||||||
|
puts '42'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
module InspecPlugins
|
||||||
|
module MeaningOfLife
|
||||||
|
|
||||||
|
class Plugin < Inspec.plugin(2)
|
||||||
|
plugin_name :meaning_of_life
|
||||||
|
# cli 'meaning-of-life-the-universe-and-everything' do
|
||||||
|
# require_relative './cli'
|
||||||
|
# InspecPlugins::MeaningOfLife::Cli
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
# 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/plugin.rb'
|
|
@ -0,0 +1,12 @@
|
||||||
|
module InspecPlugins
|
||||||
|
module MeaningOfLife
|
||||||
|
class MockPlugin < Inspec.plugin(2, :mock_plugin_type)
|
||||||
|
|
||||||
|
# Do mockish things
|
||||||
|
def execute(opts)
|
||||||
|
return 42
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
module InspecPlugins
|
||||||
|
module MeaningOfLife
|
||||||
|
|
||||||
|
class Plugin < Inspec.plugin(2)
|
||||||
|
plugin_name :'inspec-meaning-of-life'
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,7 @@ require 'minitest/autorun'
|
||||||
require 'minitest/spec'
|
require 'minitest/spec'
|
||||||
require 'mocha/setup'
|
require 'mocha/setup'
|
||||||
|
|
||||||
require 'inspec/plugins/cli'
|
require 'inspec/plugin/v1/plugin_types/cli'
|
||||||
require 'thor'
|
require 'thor'
|
||||||
|
|
||||||
describe 'plugin system' do
|
describe 'plugin system' do
|
98
test/unit/plugin/v2/api_base_test.rb
Normal file
98
test/unit/plugin/v2/api_base_test.rb
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
require 'minitest/autorun'
|
||||||
|
require 'minitest/test'
|
||||||
|
|
||||||
|
require_relative '../../../../lib/inspec/plugin/v2'
|
||||||
|
|
||||||
|
class PluginV2VersionedApiTests < MiniTest::Test
|
||||||
|
# you can call Inspec.plugin(2) and get the plugin base class
|
||||||
|
def test_calling_Inspec_dot_plugin_with_2_returns_the_plugin_base_class
|
||||||
|
klass = Inspec.plugin(2)
|
||||||
|
assert_kind_of Class, klass
|
||||||
|
assert_equal 'Inspec::Plugin::V2::PluginBase', klass.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_calling_Inspec_dot_plugin_with_2_and_mock_plugin_returns_the_mock_plugin_base_class
|
||||||
|
klass = Inspec.plugin(2, :mock_plugin_type)
|
||||||
|
assert_kind_of Class, klass, '2-arg form of Inspec.plugin() should return a specific plugin type base class'
|
||||||
|
assert_equal 'Inspec::Plugin::V2::PluginType::Mock', klass.name
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
class PluginV2BaseMgmtMethods < MiniTest::Test
|
||||||
|
def test_plugin_v2_management_class_methods_present
|
||||||
|
[
|
||||||
|
:base_class_for_type,
|
||||||
|
:registry,
|
||||||
|
:register_plugin_type,
|
||||||
|
:plugin_name,
|
||||||
|
].each do |method_name|
|
||||||
|
klass = Inspec::Plugin::V2::PluginBase
|
||||||
|
assert_respond_to klass, method_name, "Base class plugin management class method: #{method_name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_plugin_type_base_classes_can_be_accessed_by_name
|
||||||
|
klass = Inspec::Plugin::V2::PluginBase.base_class_for_type(:mock_plugin_type)
|
||||||
|
assert_kind_of Class, klass, 'base_class_for_type should work for mock_plugin_type'
|
||||||
|
assert_equal 'Inspec::Plugin::V2::PluginType::Mock', klass.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PluginV2BaseDslMethods < MiniTest::Test
|
||||||
|
def test_plugin_v2_dsl_methods_present
|
||||||
|
[
|
||||||
|
:plugin_name,
|
||||||
|
:mock_plugin_type,
|
||||||
|
# [ :attribute_provider, :platform, :fetcher, :source_reader, :control_dsl, :reporter ]
|
||||||
|
].each do |method_name|
|
||||||
|
klass = Inspec::Plugin::V2::PluginBase
|
||||||
|
assert_respond_to klass, method_name, 'Plugin DSL methods'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_when_calling_plugin_name_the_plugin_is_registered
|
||||||
|
test_plugin_name = :dsl_plugin_name_test
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
refute reg.known_plugin?(test_plugin_name), 'should not know plugin name in advance'
|
||||||
|
assert_equal 0, reg.loaded_count, 'Should start with no plugins loaded'
|
||||||
|
assert_equal 0, reg.known_count, 'Should start with no plugins known'
|
||||||
|
|
||||||
|
|
||||||
|
assert_raises(Inspec::Plugin::V2::LoadError, 'plugin definitions must include the plugin_name call') do
|
||||||
|
# Make a plugin class, including calling the plugin type DSL definition method, but do not call plugin_name
|
||||||
|
Class.new(Inspec.plugin(2)) do
|
||||||
|
# Plugin class body
|
||||||
|
mock_plugin_type :dsl_plugin_name_test do
|
||||||
|
Class.new(Inspec.plugin(2, :mock_plugin_type))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
refute reg.known_plugin?(test_plugin_name), 'failing to load a nameless plugin should not somehow register the plugin'
|
||||||
|
assert_equal 0, reg.loaded_count, 'Should have no plugins loaded after failing to load a nameless plugin'
|
||||||
|
assert_equal 0, reg.known_count, 'Should have no plugins known after failing to load a nameless plugin'
|
||||||
|
|
||||||
|
# Now create another plugin class, but this time *do* call plugin_name
|
||||||
|
name_provided_class = Class.new(Inspec.plugin(2)) do
|
||||||
|
# Plugin class body
|
||||||
|
plugin_name :dsl_plugin_name_test
|
||||||
|
mock_plugin_type :dsl_plugin_name_test do
|
||||||
|
Class.new(Inspec.plugin(2, :mock_plugin_type))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert reg.known_plugin?(test_plugin_name), 'plugin name should register the plugin'
|
||||||
|
assert_equal 0, reg.loaded_count, 'plugin_name should not load the plugin'
|
||||||
|
assert_equal 1, reg.known_count, 'plugin_name should cause one plugin to be known'
|
||||||
|
status = reg[test_plugin_name]
|
||||||
|
assert_equal name_provided_class, status.plugin_class
|
||||||
|
assert_equal 2, status.api_generation
|
||||||
|
assert_includes status.plugin_types, :mock_plugin_type
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_plugin_type_registers_an_activation_dsl_method
|
||||||
|
klass = Inspec::Plugin::V2::PluginBase
|
||||||
|
assert_respond_to klass, :mock_plugin_type, 'Activation method for mock_plugin_type'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
36
test/unit/plugin/v2/back_compat_test.rb
Normal file
36
test/unit/plugin/v2/back_compat_test.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
require 'minitest/autorun'
|
||||||
|
require 'minitest/test'
|
||||||
|
require_relative '../../../../lib/inspec'
|
||||||
|
|
||||||
|
module PluginV2BackCompat
|
||||||
|
class PluginV1TypeClassFetchers < MiniTest::Test
|
||||||
|
|
||||||
|
# Note: we can't call klass.name, because that is redefined as a setter.
|
||||||
|
|
||||||
|
# cli had a base class (which was really a registry), but no class fetcher
|
||||||
|
# There was no Inspec.plugin(...)
|
||||||
|
|
||||||
|
def test_get_plugin_v1_base_for_fetchers
|
||||||
|
klass = Inspec.fetcher(1)
|
||||||
|
assert_kind_of Class, klass
|
||||||
|
assert Inspec::Plugins.const_defined? :Fetcher
|
||||||
|
assert_equal Inspec::Plugins::Fetcher, klass
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_get_plugin_v1_base_for_source_readers
|
||||||
|
klass = Inspec.source_reader(1)
|
||||||
|
assert_kind_of Class, klass
|
||||||
|
assert Inspec::Plugins.const_defined? :SourceReader
|
||||||
|
assert_equal Inspec::Plugins::SourceReader, klass
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: rename to attribute_provider?
|
||||||
|
def test_get_plugin_v1_base_for_secrets
|
||||||
|
klass = Inspec.secrets(1)
|
||||||
|
assert_kind_of Class, klass
|
||||||
|
assert Inspec::Plugins.const_defined? :Secret
|
||||||
|
assert_equal Inspec::Plugins::Secret, klass
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
184
test/unit/plugin/v2/loader_test.rb
Normal file
184
test/unit/plugin/v2/loader_test.rb
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
# Unit tests for Inspec::PluginLoader and Registry
|
||||||
|
|
||||||
|
require 'minitest/autorun'
|
||||||
|
require 'minitest/test'
|
||||||
|
require_relative '../../../../lib/inspec/plugin/v2'
|
||||||
|
|
||||||
|
class PluginLoaderTests < MiniTest::Test
|
||||||
|
|
||||||
|
@@orig_home = Dir.home
|
||||||
|
|
||||||
|
def reset_globals
|
||||||
|
# These are effectively globals
|
||||||
|
Inspec::Plugin::V2::Registry.instance.__reset
|
||||||
|
ENV['HOME'] = @@orig_home
|
||||||
|
ENV['INSPEC_CONFIG_DIR'] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup
|
||||||
|
reset_globals
|
||||||
|
|
||||||
|
repo_path = File.expand_path(File.join( __FILE__, '..', '..', '..', '..', '..'))
|
||||||
|
mock_path = File.join(repo_path, 'test', 'unit', 'mock')
|
||||||
|
|
||||||
|
@config_dir_path = File.join(mock_path, 'config_dirs')
|
||||||
|
@bundled_plugins = [
|
||||||
|
:artifact,
|
||||||
|
:compliance,
|
||||||
|
:habitat,
|
||||||
|
:init,
|
||||||
|
:supermarket,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
reset_globals
|
||||||
|
end
|
||||||
|
|
||||||
|
#====================================================================#
|
||||||
|
# basic constructor usage and bundle detection #
|
||||||
|
#====================================================================#
|
||||||
|
|
||||||
|
def test_constructor_should_not_load_anything_automatically
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = 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
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new
|
||||||
|
@bundled_plugins.each do |bundled_plugin_name|
|
||||||
|
assert reg.known_plugin?(bundled_plugin_name), "\n#{bundled_plugin_name} should be detected as a bundled plugin"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_constructor_should_skip_bundles_when_option_is_set
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new(omit_bundles: true)
|
||||||
|
@bundled_plugins.each do |bundled_plugin_name|
|
||||||
|
refute reg.known_plugin?(bundled_plugin_name), "\n#{bundled_plugin_name} should not be detected when omit_bundles is set"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_constructor_when_using_home_dir_detects_declared_plugins
|
||||||
|
ENV['HOME'] = File.join(@config_dir_path, 'fakehome')
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new
|
||||||
|
assert reg.known_plugin?(:'inspec-test-home-marker'), "\ninspec-test-home-marker should be detected as a plugin"
|
||||||
|
end
|
||||||
|
|
||||||
|
#====================================================================#
|
||||||
|
# unusual plugin.json situations #
|
||||||
|
#====================================================================#
|
||||||
|
|
||||||
|
def test_constructor_when_the_plugin_config_is_absent_it_detects_bundled_plugins
|
||||||
|
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'empty')
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new
|
||||||
|
@bundled_plugins.each do |bundled_plugin_name|
|
||||||
|
assert reg.known_plugin?(bundled_plugin_name), "\n#{bundled_plugin_name} should be detected as a bundled plugin"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_constuctor_when_the_plugin_config_is_corrupt_it_throws_an_exception
|
||||||
|
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'corrupt')
|
||||||
|
assert_raises(Inspec::Plugin::V2::ConfigError) { Inspec::Plugin::V2::Loader.new }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_constuctor_when_the_plugin_config_is_a_bad_version_it_throws_an_exception
|
||||||
|
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'bad_plugin_conf_version')
|
||||||
|
assert_raises(Inspec::Plugin::V2::ConfigError) { Inspec::Plugin::V2::Loader.new }
|
||||||
|
end
|
||||||
|
|
||||||
|
#====================================================================#
|
||||||
|
# basic loading #
|
||||||
|
#====================================================================#
|
||||||
|
|
||||||
|
def test_load_no_plugins_should_load_no_plugins
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new(omit_bundles: true)
|
||||||
|
loader.load_all
|
||||||
|
assert_equal 0, reg.loaded_count, "\nRegistry load count"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_load_only_bundled_plugins_should_load_bundled_plugins
|
||||||
|
skip 'This keeps failing, only affects legacy bundles, will fix later'
|
||||||
|
# Skip rationale: I beleive this test is failing due to a test artifact - we
|
||||||
|
# keep loading v1 CLI plugins and then purging the registry, which results (depending
|
||||||
|
# on test order) in the Ruby `require` refusing to re-load the v1 plugin (since it was
|
||||||
|
# previously loaded). But since we purge the Registry, the Registry doesn't know
|
||||||
|
# about it either. Neither of those things are intended to happen as
|
||||||
|
# the plugin system is finished (the v1 plugins will be ported to v2, and registry
|
||||||
|
# purging should never happen in real-world use)
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new
|
||||||
|
loader.load_all
|
||||||
|
@bundled_plugins.each do |bundled_plugin_name|
|
||||||
|
assert reg.loaded_plugin?(bundled_plugin_name), "\n#{bundled_plugin_name} should be loaded"
|
||||||
|
assert_equal [ :cli_command ], reg[bundled_plugin_name].plugin_types, "annotate plugin type of bundled plugins"
|
||||||
|
assert_equal 0, reg[bundled_plugin_name].api_generation, "annotate API generation of bundled plugins"
|
||||||
|
assert_kind_of(Class, reg[bundled_plugin_name].plugin_class)
|
||||||
|
end
|
||||||
|
assert_equal @bundled_plugins.count, reg.loaded_count, "\nRegistry load count"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_load_cli_plugin_by_path
|
||||||
|
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'meaning_by_path')
|
||||||
|
reg = Inspec::Plugin::V2::Registry.instance
|
||||||
|
plugin_name = :'inspec-meaning-of-life'
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new(omit_bundles: true)
|
||||||
|
assert reg.known_plugin?(plugin_name), "\n#{plugin_name} should be a known plugin"
|
||||||
|
refute reg.loaded_plugin?(plugin_name), "\n#{plugin_name} should not be loaded yet"
|
||||||
|
loader.load_all
|
||||||
|
assert reg.loaded_plugin?(plugin_name), "\n#{plugin_name} should be loaded"
|
||||||
|
end
|
||||||
|
|
||||||
|
#====================================================================#
|
||||||
|
# activation #
|
||||||
|
#====================================================================#
|
||||||
|
def test_activation
|
||||||
|
# Setup
|
||||||
|
ENV['INSPEC_CONFIG_DIR'] = File.join(@config_dir_path, 'meaning_by_path')
|
||||||
|
registry = Inspec::Plugin::V2::Registry.instance
|
||||||
|
loader = Inspec::Plugin::V2::Loader.new(omit_bundles: true)
|
||||||
|
loader.load_all
|
||||||
|
status = registry[:'inspec-meaning-of-life']
|
||||||
|
|
||||||
|
# Management methods for activation
|
||||||
|
assert_respond_to status, :activators, 'A plugin status should respond to `activators`'
|
||||||
|
assert_respond_to registry, :find_activators, 'Registry should respond to `find_activators`'
|
||||||
|
assert_respond_to loader, :activate, 'Loader should respond to `activate`'
|
||||||
|
|
||||||
|
# Finding an Activator
|
||||||
|
assert_kind_of Array, status.activators, 'status should have an array for activators'
|
||||||
|
assert_kind_of Array, registry.find_activators(), 'find_activators should return an array'
|
||||||
|
assert_equal 'Inspec::Plugin::V2::Activator', registry.find_activators()[0].class.name, 'find_activators should return an array of Activators'
|
||||||
|
activator = registry.find_activators(plugin_type: :mock_plugin_type, name: :'meaning-of-life-the-universe-and-everything')[0]
|
||||||
|
refute_nil activator, 'find_activators should find the test activator'
|
||||||
|
[ :plugin_name, :plugin_type, :activator_name, :activated, :exception, :activation_proc, :implementation_class ].each do |method_name|
|
||||||
|
assert_respond_to activator, method_name
|
||||||
|
end
|
||||||
|
|
||||||
|
# Activation preconditions
|
||||||
|
refute activator.activated, 'Test activator should start out unactivated'
|
||||||
|
assert_nil activator.exception, 'Test activator should have no exception prior to activation'
|
||||||
|
assert_nil activator.implementation_class, 'Test activator should not know implementation class prior to activation'
|
||||||
|
refute InspecPlugins::MeaningOfLife.const_defined?(:MockPlugin), 'impl_class should not be defined prior to activation'
|
||||||
|
|
||||||
|
loader.activate(:mock_plugin_type, 'meaning-of-life-the-universe-and-everything')
|
||||||
|
|
||||||
|
# Activation postconditions
|
||||||
|
assert activator.activated, 'Test activator should be activated after activate'
|
||||||
|
assert_nil activator.exception, 'Test activator should have no exception after activation'
|
||||||
|
|
||||||
|
# facts about the implementation class
|
||||||
|
impl_class = activator.implementation_class
|
||||||
|
refute_nil impl_class, 'Activation should set the implementation class'
|
||||||
|
assert_kind_of Class, impl_class, 'Should have a Class in the implementation class slot'
|
||||||
|
assert_includes impl_class.ancestors, Inspec::Plugin::V2::PluginBase, 'impl_class should derive from PluginBase'
|
||||||
|
assert_includes impl_class.ancestors, Inspec::Plugin::V2::PluginType::Mock, 'impl_class should derive from PluginType::Mock'
|
||||||
|
assert InspecPlugins::MeaningOfLife.const_defined?(:MockPlugin), 'impl_class should now be defined'
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue