Writing robust Bolt Tasks: a guide

Hello, fearless reader! In case you missed it, we hosted a workshop for the Contributor Summit Online event to introduce best practices for writing Bolt Tasks. The workshop was a speedy general-overview, and I want to dive deeper into writing robust tasks and how the helper modules for Python and Ruby that we just released help you do just that.

What are Bolt Tasks?

In case you’re new to Bolt Tasks, they’re scripts with optional metadata that you can run on target machines in your infrastructure, allowing you to make as-needed changes to remote systems. They can be written in any language your target machines support: bash, powershell, python, ruby, etc. Tasks can be run by our open-source remote task runner Bolt, or in Puppet Enterprise, and are shared on the Puppet Forge. These principles are generally applicable, and will help whether you’re just starting out with Bolt Tasks or whether you’ve already got tasks and are looking to make them better!

How to install Task Helpers

If you’re interested in using our Python or Ruby task helper modules, the first thing to do is install them! The easiest way to do this is to include the module in a Puppetfile.

mod 'puppetlabs-ruby_task_helper’

And run bolt puppetfile install from the same directory.

You’ll also need to add the helper files to your task metadata:

"files": ["ruby_task_helper/lib/task_helper.rb"]

Check out the Bolt documentation for more information on installing modules and using them in your tasks!

Task Input

One thing that distinguishes a task from a script is it’s API - how it receives input from the task runner, and what output it produces. Task parameters can be received by the task as environment variables, as JSON from stdin, or as powershell arguments. Here’s what we recommend when parsing input:

  • Use environment variables if you have simple parameter types, like strings and integers. If you have any more complex parameters, like arrays or hashes, use stdin.
  • Stick to one input method. The default parameter input method is both, which stores the parameters as environment variables and passes them over stdin. If parameters are passed over stdin but are not consumed by the task it can cause unexpected behavior for certain edge cases. To avoid this unexpected behavior, it’s best to specify an input_method in your task metadata and ensure you consume the parameters by that method.

The task helper modules will parse input as JSON for you, so you can write task methods that accept parameters as their input like so:

require_relative '../lib/task_helper.rb'  
class MyTask < TaskHelper  
  def task(name: nil, **kwargs)  
	{ greeting: "Hi, my name is #{name}" }  
  end  
end  

This ruby script uses the ruby task helper by extending the TaskHelper class imported from the task helper module, then defines a new method task which accepts a name parameter and then any other optional parameters the task runner might supply. The name parameter is a parameter of the task we’re writing itself, while **kwargs accounts for any optional or extra parameters our task accepts. I can then refer to the parameter directly by name, instead of having to parse the JSON hash and refer to keys of that hash, and parse optional args from kwargs.

Task Output

Task output can be just about anything that’s printed during or returned from a task. You can see what happened in your task, debug your task, raise errors, and output JSON to be consumed by other tasks or programs. There are a few things to keep in mind when thinking about what output your task produces:

  • Bolt will catch any task that returns a non-zero exit code, and print stdout as the result. So if you’d like your task to error, it’s best to exit with a non-zero code and print any messages to stdout.
  • If you’re using your task as part of a plan and want to use the output, it’s best to print a single JSON object to stdout. This will be the result of the task, and can be parsed within a bolt plan

The task helper modules will catch any errors raised in the task and turn them into formatted results to return to the task runner. There’s also a TaskHelper::Error class you can use in your task to set custom error messages, types, and details.

Task Metadata

The best thing you can do to write better tasks is write good metadata! And in this case, ‘good’ means ‘as much as possible’. Good metadata is detailed, thorough, and includes as much information about the task as you can provide. Most importantly:

  • Give your task a description! This will show up in the ‘bolt task show ’ output, and help other users understand what your task does
  • Specify an input_method. As previously discussed this avoids some weird edge cases.
  • List all parameters, and give them types and descriptions. Assigning types to your parameters helps avoid malformed input (or worse, malicious input), and again you want to be as specific as possible. Instead of just “String” type, if your parameter can only be one of a few options use “Enum”, or if it matches a regex use “Pattern”. Providing descriptions also helps users know what to pass as input.
  • List implementations for the task, including any additional files the implementation might require and any requirements it has.
  • List any additional files your task depends on to run
  • Specify private or supports_noop if applicable. The private key will hide the task from bolt task show output, and the supports_noop key will indicate if your task supports running in a noop mode.
{  
	"description": "Install the Puppet 5 agent package",  
	“input_method”: “stdin”,  
	“private”: “true”,  
	"parameters": {  
  	"version": {  
    	"description": "The version of puppet-agent to install",  
    	"type": "Optional[String]"  
  	}     
	},    
	"implementations": [  
  	{"name": "install_shell.sh", "requirements": ["shell"]},  
  	{"name": "install_powershell.ps1", "requirements": ["powershell"]}  
	],  
        "files": ["ruby_task_helper/lib/task_helper.rb"]  
 }

How to test Tasks

For tasks written and tested in ruby we have the BoltSpec library, which provides functions for running bolt commands from a ruby script (basically bolt-as-a-library) and is great for integration-level testing. This post assumes you’re already familiar with testing Puppet modules, but if not we have tons of great resources for learning! In your module, you can simply install Bolt as a gem (preferably with bundler, require ‘bolt_spec/run’, include the BoltSpec::Run class in your test, then have access to helpers to run tasks, plans, and commands. Note: Bolt requires Puppet 6, so if your module is using an older version of Puppet you should include the Puppet 6 gem conditionally.

Gemfile

gem “bolt”, ‘~> 1.0’  
group(:test) do  
    gem “puppet”, ‘~> 6.0’  
End

spec_test.rb

require 'bolt_spec/run'  
  
  describe 'run_task' do  
  include BoltSpec::Run  
	it 'should run a task on a node' do  
  	result = run_task('sample::echo', 'ssh', config: config_data,  
                    	inventory: inventory_data)  
  	expect(result[0]['status']).to eq('success')  
	End  
  end  

Additionally, using the task helper modules to isolate task functionality makes it easy to test the tasks, passing in fake data and verifying the output.

Learn more

Here are some places to interact more with Bolt!

Puppet sites use proprietary and third-party cookies. By using our sites, you agree to our cookie policy.