Custom Resource Diffs in Chef
This content is more than 4 years old and the cloud moves fast so some information may be slightly out of date.
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!