Merge pull request #5829 from inspec/nm/streaming-reporters

CFINSPEC-9 Added support for streaming reporters
This commit is contained in:
Clinton Wolfe 2022-02-09 04:19:39 -05:00 committed by GitHub
commit a9960f9b81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 230 additions and 1 deletions

View file

@ -488,6 +488,72 @@ v0.1.0 - Initial version
v0.2.0 - added `run_data.profiles[0].inputs[0].options.sensitive`
v0.3.0 - added resource_name && params
## Implementing Streaming Reporter Plugins
Streaming Reporter plugins offer the opportunity to customize or create a plugin which operates real-time as the Chef Inspec tests runs. Streaming reporters perform streaming using RSpec custom formatters.
### Declare your plugin activators
In your `plugin.rb`, include one or more `streaming_reporter` activation blocks. The activation block name will be matched against the value passed into the `--reporter` option. If a match occurs, your activator will fire, which loads any needed libraries, and return your implementation class.
#### Streaming Reporter Activator Example
```ruby
# In plugin.rb
module InspecPlugins::Sweeten
class Plugin < Inspec.plugin(2)
# ... other plugin stuff
streaming_reporter :streaming_sweet do
require_relative 'streaming_reporter.rb'
InspecPlugins::Sweeten::StreamingReporter
end
end
end
```
Like any activator, the block above will only be called if needed. For Streaming Reporter plugins, the plugin system examines the `--reporter` argument, or the `reporter:` JSON config option, and looks for the activation name as a prefix. Multiple Reporter activations may occur if several different names match, though each activation will only occur once.
```bash
you@machine $ inspec exec --reporter streaming_sweet # Your Reporter implementation is activated and executed
you@machine $ inspec exec --reporter json # Your Reporter implementation is not activated
```
### Implementation class for Streaming Reporters
In your `streaming_reporter.rb`, you should begin by requesting the superclass from `Inspec.plugin`:
```ruby
module InspecPlugins::Sweeten
class StreamingReporter < Inspec.plugin(2, :streaming_reporter)
RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending
def initialize output
@output = output
end
def example_passed notification # ExampleNotification
# some logic to run on passing test
end
def example_failed notification # FailedExampleNotification
# some logic to run on failing test
end
def example_pending notification # ExampleNotification
# some logic to run on pending test
end
end
end
```
### Implementing your Streaming Reporter
A streaming reporter is a custom RSpec formatter which is used as an InSpec plugin. And it can be used for performing operations real-time using RSpec formatter methods like `example_passed`, `example_failed` and `example_pending`. Being an RSpec formatter, the method needs to be registered with `RSpec::Core::Formatters`.
This tutorial on [How to write RSpec formatters from Scratch](https://ieftimov.com/post/how-to-write-rspec-formatters-from-scratch/) will come handy.
## Implementing Input Plugins
Input plugins provide values for Chef InSpec Inputs - the parameters you can place within profile control code.

View file

@ -367,7 +367,11 @@ module Inspec
.find_activators(plugin_type: :reporter)\
.map(&:activator_name).map(&:to_s)
valid_types = rspec_built_in_formatters + inspec_reporters_that_are_not_yet_plugins + plugin_reporters
streaming_reporters = Inspec::Plugin::V2::Registry.instance\
.find_activators(plugin_type: :streaming_reporter)\
.map(&:activator_name).map(&:to_s)
valid_types = rspec_built_in_formatters + inspec_reporters_that_are_not_yet_plugins + plugin_reporters + streaming_reporters
reporters.each do |reporter_name, reporter_config|
raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name)

View file

@ -0,0 +1,10 @@
module Inspec::Plugin::V2::PluginType
class StreamingReporter < Inspec::Plugin::V2::PluginBase # TBD Superclass may need to change
register_plugin_type(:streaming_reporter)
#====================================================================#
# StreamingReporter plugin type API
#====================================================================#
# Implementation classes must implement these methods.
end
end

View file

@ -123,6 +123,8 @@ module Inspec
def set_optional_formatters
return if @conf["reporter"].nil?
# This is a slightly modified version of the default RSpec JSON formatter
# No one in their right mind should be using this because we have a much better JSON reporter - named "json"
if @conf["reporter"].key?("json-rspec")
# We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
if @conf["reporter"]["json-rspec"]&.[]("file").nil?
@ -133,6 +135,7 @@ module Inspec
@conf["reporter"].delete("json-rspec")
end
# These are built-in to rspec
formats = @conf["reporter"].select { |k, _v| %w{documentation progress html}.include?(k) }
formats.each do |k, v|
# We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
@ -143,6 +146,33 @@ module Inspec
end
@conf["reporter"].delete(k)
end
# Here we need to look for reporter names in the reporter option that
# are names of streaming reporter plugins. We load them, then tell RSpec to add them as formatters.
# They will have already been detected at this point (see v2_loader.load_all in cli.rb)
# but they will not be activated activated at this point.
# then list all plugins by type by name
reg = Inspec::Plugin::V2::Registry.instance
streaming_reporters = reg\
.find_activators(plugin_type: :streaming_reporter)\
.map(&:activator_name).map(&:to_s)
@conf["reporter"].each do |streaming_reporter_name, file_target|
# It could be a non-streaming reporter
next unless streaming_reporters.include? streaming_reporter_name
# Activate the plugin so the formatter ID gets registered with RSpec, presumably
activator = reg.find_activator(plugin_type: :streaming_reporter, activator_name: streaming_reporter_name.to_sym)
activator.activate!
# We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
if file_target&.[]("file").nil?
RSpec.configuration.add_formatter(activator.implementation_class)
else
RSpec.configuration.add_formatter(activator.implementation_class, file_target["file"])
end
@conf["reporter"].delete(streaming_reporter_name)
end
end
# Configure the output formatter and stream to be used with RSpec.

View file

@ -0,0 +1,28 @@
# StreamerBang Plugin
This plugin was generated by `inspec init plugin`, and apparently the author, 'Progress Chef InSpec Team', did not update the README.
## To Install This Plugin
Assuming it has been published to RubyGems, you can install this gem using:
```
you@machine $ inspec plugin install inspec-streamer-bang
```
## What This Plugin Does
No idea.
## Developing This Plugin
The generated plugin contains everything a real-world, industrial grade plugin would have, including:
* an (possibly incomplete) implementation of one or more InSpec Plugin Types
* documentation (you are reading it now)
* tests, at the unit and functional level
* a .gemspec, for packaging and publishing it as a gem
* a Gemfile, for managing its dependencies
* a Rakefile, for running development tasks
* Rubocop linting support for using the base InSpec project rubocop.yml (See Rakefile)

View file

@ -0,0 +1,14 @@
# This file is known as the "entry point."
# This is the file InSpec will try to load if it
# thinks your plugin is installed.
# The *only* thing this file should do is setup the
# load path, then load the plugin definition file.
# Next two lines simply add the path of the gem to the load path.
# This is not needed when being loaded as a gem; but when doing
# plugin development, you may need it. Either way, it's harmless.
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
require "inspec-streamer-bang/plugin"

View file

@ -0,0 +1,31 @@
# Plugin Definition file
# The purpose of this file is to declare to InSpec what plugin_types (capabilities)
# are included in this plugin, and provide activator that will load them as needed.
# It is important that this file load successfully and *quickly*.
# Your plugin's functionality may never be used on this InSpec run; so we keep things
# fast and light by only loading heavy things when they are needed.
# Presumably this is light
require "inspec-streamer-bang/version"
# The InspecPlugins namespace is where all plugins should declare themselves.
# The "Inspec" capitalization is used throughout the InSpec source code; yes, it's
# strange.
module InspecPlugins
module StreamerBang
class Plugin < ::Inspec.plugin(2)
plugin_name :"inspec-streamer-bang"
streaming_reporter :bang do
# Calling this activator doesn't mean the subcommand is being executed - just
# that we should be ready to do so. So, load the file that defines the
# functionality.
require "inspec-streamer-bang/streaming_reporter"
InspecPlugins::StreamerBang::StreamingReporter
end
end
end
end

View file

@ -0,0 +1,21 @@
module InspecPlugins::StreamerBang
class StreamingReporter < Inspec.plugin(2, :streaming_reporter)
RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending
def initialize(output)
@output = output
end
def example_passed(notification) # ExampleNotification
@output << "!"
end
def example_failed(notification) # FailedExampleNotification
@output << "F"
end
def example_pending(notification) # ExampleNotification
@output << "*"
end
end
end

View file

@ -0,0 +1,8 @@
# This file simply makes it easier for CI engines to update
# the version stamp, and provide a clean way for the gemspec
# to learn the current version.
module InspecPlugins
module StreamerBang
VERSION = "0.1.0".freeze
end
end

View file

@ -183,6 +183,23 @@ describe "disable plugin usage message integration" do
end
end
#=========================================================================================#
# Streaming reporter Plugin Support
#=========================================================================================#
describe "Streaming-reporter plugin type support" do
include PluginFunctionalHelper
let(:fixture_path) { File.join(profile_path, "basic_profile") }
let(:streaming_reporter_plugin_path) { File.join(mock_path, "plugins", "inspec-streamer-bang", "lib", "inspec-streamer-bang.rb") }
let(:run_result) { run_inspec_with_plugin("exec #{fixture_path} --reporter bang", plugin_path: streaming_reporter_plugin_path) }
it "runs the streaming reporter plugin type successfully" do
_(run_result.stderr).must_be_empty
_(run_result.exit_status).must_equal 0
end
end
#=========================================================================================#
# DSL Plugin Support
#=========================================================================================#