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!

Similar Posts You Might Enjoy

Target Mode with Serial Devices

Target Mode with Serial Devices Usually, you will work with SSH or WinRM to connect to remote nodes and configure them. Those standard protocols bring along all the perks of a modern network connection: Encryption, Authentication, File transfers, etc But what if you have a device without network connectivity? - by Thomas Heinen

Writing Chef Target Mode Resources

Writing Chef Target Mode Resources After my previous blog posts, you might be tempted to write your own Chef custom resources which are compatible with Target Mode. Luckily, this is very easy - so this will be a short one. - by Thomas Heinen

Local Preprocessing in Target Mode

Local Preprocessing in Target Mode If you ever created configuration files with any automation system, you know that this involves a lot of templating. This is actually one of the most basic tasks that Chef performs and it is done using the template resource. With Chef’s Target Mode this currently is a bit more complicated. - by Thomas Heinen