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>
2018-08-16 22:16:32 +00:00
|
|
|
# Functional tests related to plugin facility
|
|
|
|
require 'functional/helper'
|
|
|
|
|
2018-11-16 22:03:40 +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>
2018-08-16 22:16:32 +00:00
|
|
|
#=========================================================================================#
|
|
|
|
# Loader Errors
|
|
|
|
#=========================================================================================#
|
|
|
|
describe 'plugin loader' do
|
|
|
|
include FunctionalHelper
|
|
|
|
|
|
|
|
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
|
2018-08-17 00:22:28 +00:00
|
|
|
|
|
|
|
#=========================================================================================#
|
|
|
|
# CliCommand plugin type
|
|
|
|
#=========================================================================================#
|
|
|
|
describe 'cli command plugins' do
|
|
|
|
include FunctionalHelper
|
|
|
|
|
|
|
|
it 'is able to respond to a plugin-based cli subcommand' do
|
|
|
|
outcome = inspec_with_env('meaningoflife answer', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'meaning_by_path'))
|
|
|
|
outcome.stderr.wont_include 'Could not find command "meaningoflife"'
|
|
|
|
outcome.stderr.must_equal ''
|
|
|
|
outcome.stdout.must_equal ''
|
|
|
|
outcome.exit_status.must_equal 42
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is able to respond to [help subcommand] invocations' do
|
|
|
|
outcome = inspec_with_env('help meaningoflife', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'meaning_by_path'))
|
|
|
|
outcome.exit_status.must_equal 0
|
|
|
|
outcome.stderr.must_equal ''
|
|
|
|
outcome.stdout.must_include 'inspec meaningoflife answer'
|
|
|
|
# Full text:
|
|
|
|
# 'Exits immediately with an exit code reflecting the answer to life the universe, and everything.'
|
|
|
|
# but Thor will ellipsify based on the terminal width
|
|
|
|
outcome.stdout.must_include 'Exits immediately'
|
|
|
|
end
|
|
|
|
|
|
|
|
# This is an important test; usually CLI plugins are only activated when their name is present in ARGV
|
|
|
|
it 'includes plugin-based cli commands in top-level help' do
|
|
|
|
outcome = inspec_with_env('help', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'meaning_by_path'))
|
|
|
|
outcome.exit_status.must_equal 0
|
|
|
|
outcome.stdout.must_include 'inspec meaningoflife'
|
|
|
|
end
|
2018-09-18 04:00:54 +00:00
|
|
|
end
|
2018-09-25 14:29:18 +00:00
|
|
|
|
|
|
|
#=========================================================================================#
|
|
|
|
# inspec plugin command
|
|
|
|
#=========================================================================================#
|
|
|
|
# See lib/plugins/inspec-plugin-manager-cli/test
|
2018-09-25 22:51:38 +00:00
|
|
|
|
|
|
|
#=========================================================================================#
|
|
|
|
# CLI Usage Messaging
|
|
|
|
#=========================================================================================#
|
|
|
|
describe 'plugin cli usage message integration' do
|
|
|
|
include FunctionalHelper
|
|
|
|
|
|
|
|
[' help', ''].each do |invocation|
|
|
|
|
it "includes v2 plugins in `inspec#{invocation}` output" do
|
|
|
|
outcome = inspec(invocation)
|
|
|
|
outcome.stderr.must_equal ''
|
|
|
|
|
|
|
|
# These are some subcommands provided by core v2 plugins
|
|
|
|
['habitat', 'artifact'].each do |subcommand|
|
|
|
|
outcome.stdout.must_include('inspec ' + subcommand)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-09-27 22:46:35 +00:00
|
|
|
|
Plugin Type: DSLs (#3557)
This PR adds 5 closely related plugin types, which allow a plugin to implement new DSL methods / keywords. The mechanism to activate the plugins are all very similar - basically, in a particular location in the code, `method_missing` is implemented, and is used to activate the particular type of DSL being requested.
4 of the DSL plugin types relate to code that could appear in a profile control file.
* outer_profile_dsl plugins allow you to extend the code in profile Ruby files that appear outside `control` or `describe` blocks.
* control_dsl plugins allow you to extend the code within `control` blocks.
* describe_dsl plugins allow you to extend the code within `describe` blocks.
* test_dsl plugins allow you to extend the code within `it`/`its` blocks.
Finally, the `resource_dsl` plugin allows you to extend the code used within custom resources.
Basic unit tests are provided to prove that the plugin types are properly defined.
A simple plugin fixture defining DSL hooks (based on favorite foods) is included, and is exercised through a set of functional tests.
The plugin developer docs are updated to describe the 5 DSLs.
*Note*: Implementing a plugin using any of the DSL plugin types is experimental. The contexts that are exposed to the DSL methods are private and poorly documented. The InSpec project does not claim the APIs used by these plugin types are covered by SemVer. Plugin authors are encouraged to pin tightly to the `inspec` gem in their gemspecs.
Motivation for this plugin comes from the desire to allow passionate community members to implement things like "2 out of 3" tests, example groups, improved serverspec compatibility, "they/their" and other "fluency" changes, as well as make it possible for future work by the InSpec team to be implemented as a core plugin, rather than a direct change to the main codebase.
2018-11-29 19:14:06 +00:00
|
|
|
#=========================================================================================#
|
|
|
|
# DSL Plugin Support
|
|
|
|
#=========================================================================================#
|
|
|
|
|
|
|
|
describe 'DSL plugin types support' do
|
|
|
|
include PluginFunctionalHelper
|
|
|
|
|
|
|
|
let(:fixture_path) { File.join(profile_path, 'dsl_plugins', 'controls', profile_file)}
|
|
|
|
let(:dsl_plugin_path) { File.join(mock_path, 'plugins', 'inspec-dsl-test', 'lib', 'inspec-dsl-test.rb')}
|
|
|
|
let(:run_result) { run_inspec_with_plugin("exec #{fixture_path}", plugin_path: dsl_plugin_path) }
|
|
|
|
let(:json_result) { run_result.payload.json }
|
|
|
|
|
|
|
|
describe 'outer profile dsl plugin type support' do
|
|
|
|
let(:profile_file) { 'outer_profile_dsl.rb' }
|
|
|
|
it 'works correctly with outer_profile dsl extensions' do
|
|
|
|
run_result.stderr.must_equal ''
|
|
|
|
|
|
|
|
# The outer_profile_dsl.rb file has control-01, then a call to favorite_grain
|
|
|
|
# (which generates a control), then control-03.
|
|
|
|
# If the plugin exploded, we'd see control-01 but not control-03
|
|
|
|
controls = json_result['profiles'][0]['controls']
|
|
|
|
controls.count.must_equal 3
|
|
|
|
|
|
|
|
# We expect the second controls id to be 'sorghum'
|
|
|
|
# (this is the functionality of the outer_profile_dsl we installed)
|
|
|
|
generated_control = json_result['profiles'][0]['controls'][1]
|
|
|
|
generated_control['id'].must_equal 'sorghum'
|
|
|
|
generated_control['results'][0]['status'].must_equal 'passed'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'control dsl plugin type support' do
|
|
|
|
|
|
|
|
let(:profile_file) { 'control_dsl.rb' }
|
|
|
|
it 'works correctly with control dsl extensions' do
|
|
|
|
run_result.stderr.must_equal ''
|
|
|
|
|
|
|
|
# The control_dsl.rb file has one control, with a describe-01, then a call to favorite_fruit, then describe-02
|
|
|
|
# If the plugin exploded, we'd see describe-01 but not describe-02
|
|
|
|
results = json_result['profiles'][0]['controls'][0]['results']
|
|
|
|
results.count.must_equal 2
|
|
|
|
|
|
|
|
# We expect the descriptions to include that the favorite fruit is banana
|
|
|
|
# (this is the functionality of the control_dsl we installed)
|
|
|
|
first_description_section = json_result['profiles'][0]['controls'][0]['descriptions'].first
|
|
|
|
first_description_section.wont_be_nil
|
|
|
|
first_description_section['label'].must_equal 'favorite_fruit'
|
|
|
|
first_description_section['data'].must_equal 'Banana'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'describe dsl plugin type support' do
|
|
|
|
let(:profile_file) { 'describe_dsl.rb' }
|
|
|
|
it 'works correctly with describe dsl extensions' do
|
|
|
|
run_result.stderr.must_equal ''
|
|
|
|
|
|
|
|
# The describe_dsl.rb file has one control, with
|
|
|
|
# describe-01, describe-02 which contains a call to favorite_vegetable, then describe-03
|
|
|
|
# If the plugin exploded, we'd see describe-01 but not describe-02
|
|
|
|
results = json_result['profiles'][0]['controls'][0]['results']
|
|
|
|
results.count.must_equal 3
|
|
|
|
|
|
|
|
# We expect the description of describe-02 to include the word aubergine
|
|
|
|
# (this is the functionality of the describe_dsl we installed)
|
|
|
|
second_result = json_result['profiles'][0]['controls'][0]['results'][1]
|
|
|
|
second_result.wont_be_nil
|
|
|
|
second_result['code_desc'].must_include 'aubergine'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'test dsl plugin type support' do
|
|
|
|
let(:profile_file) { 'test_dsl.rb' }
|
|
|
|
it 'works correctly with test dsl extensions' do
|
|
|
|
run_result.stderr.must_equal ''
|
|
|
|
|
|
|
|
# The test_dsl.rb file has one control, with
|
|
|
|
# describe-01, describe-02 which contains a call to favorite_legume, then describe-03
|
|
|
|
# If the plugin exploded, we'd see describe-01 but not describe-02
|
|
|
|
results = json_result['profiles'][0]['controls'][0]['results']
|
|
|
|
results.count.must_equal 3
|
|
|
|
|
|
|
|
# I spent a while trying to find a way to get the test to alter its name;
|
|
|
|
# that won't work for various setup reasons.
|
|
|
|
# So, it just throws an exception with the word 'edemame' in it.
|
|
|
|
second_result = json_result['profiles'][0]['controls'][0]['results'][1]
|
|
|
|
second_result.wont_be_nil
|
|
|
|
second_result['status'].must_equal 'failed'
|
|
|
|
second_result['message'].must_include 'edemame'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'resource dsl plugin type support' do
|
|
|
|
let(:profile_file) { 'unused' }
|
|
|
|
it 'works correctly with test dsl extensions' do
|
|
|
|
# We have to build a custom command line - need to load the whole profile,
|
|
|
|
# so the libraries get loaded.
|
|
|
|
cmd = 'exec '
|
|
|
|
cmd += File.join(profile_path, 'dsl_plugins')
|
|
|
|
cmd += ' --controls=/^rdsl-control/ '
|
|
|
|
run_result = run_inspec_with_plugin(cmd, plugin_path: dsl_plugin_path)
|
|
|
|
run_result.stderr.must_equal ''
|
|
|
|
|
|
|
|
# We should have three controls; 01 and 03 just do a string match.
|
|
|
|
# 02 uses the custom resource, which relies on calls to the resource DSL.
|
|
|
|
# If the plugin exploded, we'd see rdsl-control-01 but not rdsl-control-02
|
|
|
|
json_result = run_result.payload.json
|
|
|
|
results = json_result['profiles'][0]['controls']
|
|
|
|
results.count.must_equal 3
|
|
|
|
|
|
|
|
# Control 2 has 2 describes; one uses a simple explicit matcher,
|
|
|
|
# while the second uses a matcher defined via a macro provided by the resource DSL.
|
|
|
|
control2_results = results[1]['results']
|
|
|
|
control2_results[0]['status'].must_equal 'passed'
|
|
|
|
control2_results[0]['code_desc'].must_include 'favorite_berry'
|
|
|
|
control2_results[0]['code_desc'].must_include 'blendable'
|
|
|
|
|
|
|
|
control2_results[1]['status'].must_equal 'passed'
|
|
|
|
control2_results[1]['code_desc'].must_include 'favorite_berry'
|
|
|
|
control2_results[1]['code_desc'].must_include 'have drupals'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-27 22:46:35 +00:00
|
|
|
#=========================================================================================#
|
|
|
|
# Train Plugin Support
|
|
|
|
#=========================================================================================#
|
|
|
|
|
|
|
|
describe 'train plugin support' do
|
|
|
|
describe 'when a train plugin is installed' do
|
|
|
|
include FunctionalHelper
|
|
|
|
it 'can run inspec detect against a URL target' do
|
|
|
|
outcome = inspec_with_env('detect -t test-fixture://', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'train-test-fixture'))
|
|
|
|
outcome.exit_status.must_equal(0)
|
|
|
|
outcome.stderr.must_be_empty
|
|
|
|
lines = outcome.stdout.split("\n")
|
|
|
|
lines.grep(/Name/).first.must_include('test-fixture')
|
|
|
|
lines.grep(/Name/).first.wont_include('train-test-fixture')
|
|
|
|
lines.grep(/Release/).first.must_include('0.1.0')
|
|
|
|
lines.grep(/Families/).first.must_include('os')
|
|
|
|
lines.grep(/Families/).first.must_include('windows')
|
|
|
|
lines.grep(/Families/).first.must_include('unix')
|
|
|
|
lines.grep(/Arch/).first.must_include('mock')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'can run inspec detect against a test-fixture backend' do
|
|
|
|
outcome = inspec_with_env('detect -b test-fixture', INSPEC_CONFIG_DIR: File.join(config_dir_path, 'train-test-fixture'))
|
|
|
|
outcome.exit_status.must_equal(0)
|
|
|
|
outcome.stderr.must_be_empty
|
|
|
|
lines = outcome.stdout.split("\n")
|
|
|
|
lines.grep(/Name/).first.must_include('test-fixture')
|
|
|
|
lines.grep(/Name/).first.wont_include('train-test-fixture')
|
|
|
|
lines.grep(/Release/).first.must_include('0.1.0')
|
|
|
|
lines.grep(/Families/).first.must_include('os')
|
|
|
|
lines.grep(/Families/).first.must_include('windows')
|
|
|
|
lines.grep(/Families/).first.must_include('unix')
|
|
|
|
lines.grep(/Arch/).first.must_include('mock')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'can run inspec shell and read a file' do
|
|
|
|
outcome = inspec_with_env("shell -t test-fixture:// -c 'file(\"any-path\").content'", INSPEC_CONFIG_DIR: File.join(config_dir_path, 'train-test-fixture'))
|
|
|
|
outcome.exit_status.must_equal(0)
|
|
|
|
outcome.stderr.must_be_empty
|
|
|
|
outcome.stdout.chomp.must_equal 'Lorem Ipsum'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'can run inspec shell and run a command' do
|
|
|
|
outcome = inspec_with_env("shell -t test-fixture:// -c 'command(\"echo hello\").exit_status'", INSPEC_CONFIG_DIR: File.join(config_dir_path, 'train-test-fixture'))
|
|
|
|
outcome.exit_status.must_equal(0)
|
|
|
|
outcome.stderr.must_be_empty
|
|
|
|
outcome.stdout.chomp.must_equal "17"
|
|
|
|
|
|
|
|
outcome = inspec_with_env("shell -t test-fixture:// -c 'command(\"echo hello\").stdout'", INSPEC_CONFIG_DIR: File.join(config_dir_path, 'train-test-fixture'))
|
|
|
|
outcome.exit_status.must_equal(0)
|
|
|
|
outcome.stderr.must_be_empty
|
|
|
|
outcome.stdout.chomp.must_equal "Mock Command Result stdout"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|