Under certain conditions, I need to abort/end a Chef run with a non-zero status code, that will then propagate back through our deployment chain and eventually to Jenkins resulting in a big, fat red ball.
What is the best way to do this?
Under certain conditions, I need to abort/end a Chef run with a non-zero status code, that will then propagate back through our deployment chain and eventually to Jenkins resulting in a big, fat red ball.
What is the best way to do this?
For the readers coming to this question and answer in the future that may not be familiar with Chef, a Chef run "converges" the node, or brings it in line with the policy declared in the recipe(s) it is running. This is also called "convergence." This has two phases, "compile" and "execute." The compile phase is when Chef evaluates ("compiles") the recipes' Ruby code, looking for resources to add to the Resource Collection. Once that is complete, it "executes" the actions for each resource to put it into the state desired. System commands are run, etc.
Erik Hollensbe wrote an excellent walk through of how this works in 2013.
Now, for the answer:
There are several ways to end a Chef run, or exit a Chef recipe, depending on how you want to go about it, since Chef recipes are Ruby code.
If your goal is to stop processing a recipe based on a condition, but continue with the rest of the run, then use the return
Ruby keyword. For example:
file '/tmp/ponies' do
action :create
end
return if node['platform'] == 'windows'
package 'bunnies-and-flowers' do
action :install
end
We presume that if the system is Windows, it doesn't have a package manager that can install the bunnies-and-flowers package, so we return.
If you wish to abort the Chef run entirely
There are a couple other things you can do. Chef exits if it encounters an unhandled exception anywhere in the Chef run. For example, if a template resource can't find its source file, or if the user running Chef doesn't have permission to do something like make a directory. This is why using raise
works to end a run.
Where you put raise
matters. If you use it in a ruby_block
resource, it will only raise during the execution phase in convergence. If you use it outside of a resource like the return
example above, it will happen during the compile phase.
file '/tmp/ponies' do
action :create
end
raise if node['platform'] == 'windows'
package 'bunnies-and-flowers' do
action :install
end
Perhaps we do have a package manager on Windows, and we want this package installed. The raise will result in Chef fatally exiting and giving a stack trace.
Another approach is to use Chef::Application.fatal!
. This logs a fatal
message to the Chef logger and STDERR, and exits the application. You can also give it a return code (maybe you have a script that checks those?).
Chef::Application.fatal!("Didn't expect the Spanish Inquistion", 42) if spanish_inquisition
(of course spanish_inquisition
is usually nil since no one expects it... I digress...)
This will result in Chef exiting, the log message sent, and the return code 42 from the process.
Note: This causes the entire application to exit, meaning if it is running as a daemonized service, it will terminate, and depending on how the service is managed, it may or may not start again. For example, an init.d
service won't restart, but a runit
service will.
Since recipes are Ruby, you can also gracefully handle error conditions with a begin..rescue
block.
begin
dater = data_bag_item(:basket, "flowers")
rescue Net::HTTPServerException
# maybe some retry code here?
raise "Couldn't find flowers in the basket, need those to continue!"
end
data_bag_item
makes an HTTP request for a data bag on the Chef Server, and will return a Net::HTTPServerException
if there's a problem from the server (404 not found, 403 unauthorized, etc). We could possibly attempt to retry or do some other handling, and then fall back to raise
.
Reporting Errors
Simply exiting and tossing a stack trace is fine if you're running Chef from the command-line. However, if you're running it in cron or as a daemon across a few, or even dozens or hundreds of machines, this isn't a great way to keep sanity when there's problems.
Enter Chef's report/exception handler feature. You can use a handler for your Chef runs. All report handlers are run at the end of a Chef run. Exception handlers are run at the end of an aborted Chef run. The status of the run is tracked, and can be checked in the handler, so you can write one that handles both kinds of run (successful/completed or unsuccessful/aborted).
The documentation tells you how to write one. It also includes a list of available open source handlers that you can use for a variety of services, including:
And several more.
The recommended way to abort or edit a Chef run is to raise an exception. Here's an example:
ruby_block "some tricky operation" do
block do
OperationFoo
raise "Operation Foo Failed" if some_condition
end
end
Chef::Application.fatal! should do what your looking for. Here is an example from our code base which might be helpful.
cipher = case key.length
when 16 then "AES-128-ECB"
when 24 then "AES-192-ECB"
when 32 then "AES-256-ECB"
else
Chef::Application.fatal!("AES Key must be 16, 24, or 32 characters in length but key #{key} has length of #{key.length}")
end
Just use below statement when you want chef
to finish after some action:
throw :end_client_run_early
It will exit without any error.
To do an unclean exit during a chef-solo run, try this:
bash 'exit' do
code 'killall -9 chef-solo'
end