Custom Resource Diffs in Chef
Custom Resource Diffs in Chef
If you are writing custom resources regularly, you might have been annoyed by a general “diff” functionality in Chef. In this post we will work on some snippets to make this possible
While the file
and template
resources will output an overview of added/removed/changed lines during the Chef run, there is no built-in facility for your own resources.
In my current project I am working heavily with Chef Target Mode (you might have noticed from the last posts), so I cannot simply use the existing resources for change output.
Current state
Chef bundles the Diff::LCS gem for it’s output on the mentioned resources, so there is no external dependency needed.
When searching for built-in functionality, I discovered some code lines in Chef::ResourceReporter and Chef::DataCollector::RunEndMessage which are checking if a resource responds to a diff
method call. Sadly, I was not able to make this work.
I also found that the existing code in Chef::Util::Diff does only work with local files and not string input.
Customized Diff Method
Our new method is basically some copy & paste from the Chef::Util::Diff#udiff resource with it’s file-specific lines removed.
def str_udiff(old_data, new_data)
diff_str = ""
file_length_difference = 0
diff_data = ::Diff::LCS.diff(old_data, new_data)
return diff_str if old_data.empty? && new_data.empty?
return "No differences encountered\n" if diff_data.empty?
# loop over diff hunks. if a hunk overlaps with the last hunk,
# join them. otherwise, print out the old one.
old_hunk = hunk = nil
diff_data.each do |piece|
begin
hunk = ::Diff::LCS::Hunk.new(old_data, new_data, piece, 3, file_length_difference)
file_length_difference = hunk.file_length_difference
next unless old_hunk
next if hunk.merge(old_hunk)
diff_str << old_hunk.diff(:unified) << "\n"
ensure
old_hunk = hunk
end
end
diff_str << old_hunk.diff(:unified) << "\n"
diff_str
end
So by passing in the current and new values into this function, we will get the output we want. Please be aware, that Diff::LCS expects this input to be an array of lines, not a String.
Using this helper is easy in our custom resources:
description = ["update my configuration"]
description << str_udiff(
@current_resource.content.lines(chomp:true),
@new_resource.content.lines(chomp:true)
)
converge_by(description) do
# Do your work
end
As the lines
method preserves line endings, the str_udiff
code would result in double newlines. Luckily, there is the chomp
option available to fix that.
By using converge_by
you can supply a custom description in a custom resource, which is not documented very well.
Getting Fancy
The only thing I was not happy about with this solution is the missing eye candy. I would love to have some output which marks removed lines in red and added lines in green. While I was browsing for Rubygems to use (before realizing Chef already bundled something), I found the excellent samg/diffy tool. This one already includes some code for coloring diff output in its diffy/format.rb file.
We can use that code with slight adjustments to have a diff function with ANSI colors:
def diff(current, new)
diff = Chef::Util::Diff.new.str_udiff(
current.lines(chomp: true),
new.lines(chomp: true)
)
diff.lines.map do |line|
case line
when /^(---|\+\+\+|\\\\)/
"\033[90m#{line.chomp}\033[0m"
when /^\+/
"\033[32m#{line.chomp}\033[0m"
when /^-/
"\033[31m#{line.chomp}\033[0m"
when /^@@/
"\033[36m#{line.chomp}\033[0m"
else
"\033[0m#{line.chomp}"
end
end.join("\n") + "\n"
end
As this one does the conversion of String into Arrays for us, our use inside the custom resource gets even easier:
description = ["update my configuration"]
description << diff(@current_resource.content, @new_resource.content)
converge_by(description) do
# Do your work
end
Have fun!