Custom Resource Diffs in Chef

Thumbnail

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!