Table of Contents generated with DocToc
- Plugin Development Guide
The puppet debugger now features a plugin system. This was added to support future expansion via third party developers.
At this time there is only a single type of plugin called InputResponder, but in the future there will be additional types of plugins.
There are two ways to package a plugin.
- create a core plugin to be merged into the puppet-debugger codebase
- create a external plugin packaged as a gem and distributed via gem server such as rubygems.org
If you think your plugin should be in the core please create a PR and ensure you have unit test coverage for your plugin.
For reference you can use the following doc How to create a gem
-
Create the gem via
bundle gem <plugin_name>
bundle gem --test=rspec fancy_plugin cd fancy_plugin mkdir -p lib/plugins/puppet-debugger/input_responders
-
Add the following to your Gemfile
group :dev, :test do gem 'puppet-debugger' gem 'pry' gem 'CFPropertyList' gem 'rake' gem 'rspec', '>= 3.6' # loads itself so you don't have to update RUBYLIB path gem 'your_plugin', path: './' end
-
bundle install
-
Follow the New Plugin Instructions
-
Version the gem
-
Package and push the gem to rubygems.
-
Tell others about it.
In order to test your plugin gem with the puppet-debugger you will need to add your gem's lib path to the RUBYLIB environment variable.
RUBYLIB=~/path_to_gem/lib:$RUBYLIB puppet debugger
Once this is set, puppet-debugger will discover your gem automatically and you should see it in the commands list.
Note: if you add the your plugin to the Gemfile as shown above in step 2 there is no need to set the RUBYLIB variable.
- Fork the puppet-debugger repo
- Follow the New Plugin Instructions
- Submit a PR
-
Create a file with the name of your plugin lib/plugins/puppet-debugger/input_responders/fancy_plugin.rb
-
Add the following content to the plugin.
require 'puppet-debugger/input_responder_plugin' module PuppetDebugger module InputResponders class Fancy < InputResponderPlugin COMMAND_WORDS = %w(fancy) SUMMARY = 'This is a fancy plugin' COMMAND_GROUP = :tools def run(args = []) 'hello from a fancy plugin' end end end end
-
ensure the class name is the same as the file name which follows ruby best practices
-
Add words to the COMMAND_WORDS constant which will be used to run your plugin from the debugger.
-
Add a short summary that describes your plugin's functionality
-
Add a group to which your plugin should belong to. This appears in the
commands
plugin output. -
You must implement the Run Method. This method is called when your plugin's command word is entered.
-
Write unit tests to validate your code works.
You can review the Required Constants docs for more info.
In order for you plugin to be discovered you must create this exact directly layout. Your plugin file must be in
the following directory lib/plugins/puppet-debugger/input_responders/
If you are packaging as a gem you must still provide this directory layout in addition to whatever other supporting files are also in your gem.
Your plugin must override the run method. When your plugin is executed, an array is passed as the args variable.
This variable contains all the arguments that can be supplied to your plugin. It is not required that you utilize
the args
variable as some plugins run without arguments but it must be the only argument.
For example: fancy hello there sir
would be passed as ['hello', 'there', 'sir'] to your plugin's run method.
def run(args = [])
greeting = args.first
"#{greeting} from a fancy plugin"
end
These are the words the user will enter to interact with your plugin. You can provide multiple words but only the first word will show up in the commands help screen.
Example:
2:>> classes
[
[0] "settings",
[1] "__node_regexp__foo"
]
2:>>
Ensure you set the following in your plugin class
COMMAND_WORDS = %w(fancy werd)
Set the Summary Constant to tell users what your plugin does. This will show up in the commands help screen.
Tools
fancy This is a fancy plugin that does nothing
SUMMARY = 'This is a fancy plugin that does nothing'
The group name appears on the commands help screen and categories tools
based on the value of the command_group constant ie. COMMAND_GROUP = :tools
Below is a list of groups you can use to categorize your plugin. Groups are created dynamically by simply supplying a new group name.
:help
:tools
:scope
:node
:environment
:editing
:context
Every plugin has access to debugger's central objects. You may need to use these objects to implement your plugin.
Objects exposed that you might want access to:
- debugger (direct use is not recommended)
- scope (The puppet scope object)
- node (The puppet node object)
- environment (The puppet environment object)
- facts (The puppet facts hash)
- compiler (The puppet compiler object)
- catalog (The puppet catalog)
- function_map (Current map of functions)
- loaders ( The puppet loaders )
- puppet_environment
- add_hook, delete_hook
- handle_input (use instead of debugger.handle_input)
While you do have access to the debugger
object itself and everything inside this object. I would recommend not using the debugger
object directly since the debugger code base is changing rapidly. Usage can result in a broken plugin. If you are using
the debugger object directly please open an issue so we can create a interface for your use case to provide future compatibility.
In addition the plugin API you can run code during certain events in the debugger lifecycle. This allows you to run your plugin code only when certain actions occur. Please remember that your hook's code will be run multiple times during the debugger's session.
If your hook code takes a while to run, please ensure it runs fast or throw the code into a separate thread if applicable.
Below is a list of the current events that you can hook into.
- after_output (After the debugger has returned control back to the console)
- before_eval (Occurs before puppet evaluates the code)
- after_eval (Occurs after puppet evaluates the code and before the debugger sends the output to the console)
To hook into a debugger event you just add a hook via the add_hook
method with the name of the event you wish to hook into.
An example of this pattern is below. In this example, when graph
is entered by the user, the plugin toggles the execution
of creating a graph after the output is sent to the console. The toggle either adds or deletes the hook. Since creating the graph
can take a while we also create a thread so we don't hold the console hostage. A new graph is created each time a puppet evaluation occurs.
def run(args = [])
toggle_status
end
def toggle_status
status = !status
if status
add_hook(:after_eval, :create_graph) do |code, debugger|
# ensure we only start a single thread, otherwise they could stack up
# and try to write to the same file.
Thread.kill(@graph_thread) if @graph_thread
@graph_thread = Thread.new { create_html(create_graph_content) }
end
out = "Graph mode enabled at #{get_url}"
else
delete_hook(:after_output, :create_graph_content)
out = "Graph mode disabled"
end
out
end
There are two ways to call other plugins.
You can call another plugin via the handle_input method ie. debugger.handle_input('help')
. Just use the plugin command word
and any arguments that it takes to call the plugin.
This makes the debugger handle the loading of the plugin and returns formatted output which is most of the time what you want. This does not send any output to the console so it is up to you to decide what to do next.
Should you want to call the plugin directly you can bypass the handle_input
method and use the plugin_from_command
to return the plugin instance.
# get a plugin instance
play_plugin = PuppetDebugger::InputResponders::Commands.plugin_from_command('play')
# execute the plugin
args = ['https://gists.github.com/sdalfsdfadsfds.txt']
# pass an instance of the debugger (always do this)
output = play_plugin.execute(args, debugger)
If the command used to find the plugin is incorrect a PuppetDebugger::Exception::InvalidCommand
error will be raised.
If your plugin takes extra arguments you may want to incorporate command completion to help the user. This can be done
by overriding the self.command_completion
function in your plugin. The return value of this method must return an
array of possible words.
# @param buffer_words [Array[String]] a array of words the user has typed in
# @return Array[String] - an array of words that will help the user with word completion
# By default this returns an empty array, your plugin can chose to override this method in order to
# provide the user with a list of key words based on the user's input
def self.command_completion(buffer_words)
['one', 'two', 'three']
end
A real example of this can be found in the set plugin
KEYWORDS = %w(node loglevel)
LOGLEVELS = %w(debug info)
def self.command_completion(buffer_words)
next_word = buffer_words.shift
case next_word
when 'loglevel'
if buffer_words.count > 0
LOGLEVELS.grep(/^#{Regexp.escape(buffer_words.first)}/)
else
LOGLEVELS
end
when 'debug', 'info','node'
[]
when nil
%w(node loglevel)
else
KEYWORDS.grep(/^#{Regexp.escape(next_word)}/)
end
end
- Create a new rspec test file as
spec/input_responders/plugin_name_spec.rb
At a minimum you will need the following test code. By including the shared examples `plugin_tests' you will automatially inherit some basic tests for your plugin. However, you will need to further test your code by creating additional tests.
Replace :plugin_name
with the name of your plugin command word.
require 'spec_helper'
require 'puppet-debugger'
require 'puppet-debugger/plugin_test_helper'
describe :plugin_name do
include_examples "plugin_tests"
let(:args) { [] }
# you must test your run implementation similar to this, if you have args please set them in the args let blocks
it 'works' do
expect(plugin.run(args)).to eq('????')
end
end
There are plenty of examples of plugins that are in the core code base. See lib/plugins/puppet-debugger/input_responders for examples.