Chef Provides a comprehensive list of events and different ways to handle them. These are decently documented at https://docs.chef.io/handlers.
In a recent use case, I had to ensure that a folder has only the files I wanted it to have, any more files or directories should get deleted whenever the chef-client runs. Similarly, if a file is missing, it should be re-created. In addition, I must know the names of the files that were created/deleted and the name and the content of the file that was updated.
Problem 1 - How to find which files you don't want?
There's no easy way chef provides this, so a bit of Ruby code to the rescue.
# The path where all my source files are stored.
# For me the source is the cookbook itself.
source_path = "#{__dir__}/../files/default/myfiles"
# Path on Target; where I need all my files
# Set some temporary variables
target_files = []
# Deleted Files
deleted_files = []
# updated_files
updated_files = {}
# The code to populate our source files dynamically
Dir.chdir(source_path) do
source_files = Dir.glob("*")
end
# Ensure Target Directory Exists
directory target_path
# List of Files on Target
begin
Dir.chdir(target_path) do
target_files = Dir.glob("*")
end
rescue
Chef::Log.info "Directory may not exists, skipping as non-fatal"
end
files_to_delete = target_files - source_files
Chef::Log.info "Total Source Files: #{source_files.length}"
Chef::Log.info "Total Target Files: #{target_files.length}"
Chef::Log.info "Files to delete: #{files_to_delete}"
Problem 2 - How to find which files were created/deleted/modified?
This is where the Chef handler events kick in. I prefer using the 'on event' style.
# Add the Resource Handler
Chef.event_handler do
on :resource_updated do |resource, action, update|
begin
if resource.is_a?(Chef::Resource::File) or resource.is_a?(Chef::Resource::CookbookFile) or resource.is_a?(Chef::Resource::Directory)
if action == :delete
deleted_files << resource.path
elsif action == :create
updated_files.store(resource.path, resource.diff)
end
end
rescue
# do nothing
end
end
end
The code above will get triggered everytime a resource is updated (any resource). Chef provides different events which you can trap, here I was interested to know only about the updates. I was also only interested in if the resource was a File or a Directory.
Bringing the last bits together.
Once I knew which files to delete, I could just delete them and the event above would keep a track of which files were deleted. Similarly for file creation and modification.
files_to_delete.each do | f |
if File.directory?("#{target_path}/#{f}")
directory "#{target_path}/#{f}" do
recursive true
action :delete
end
else
file "#{target_path}/#{f}" do
action :delete
end
end
end
source_files.each do | f |
cookbook_file "#{target_path}/#{f}" do
source "#{f}"
end
end
# Finally, log a message with all files deleted.
# Notice the use of lazy block to evaluate the variable at converge time.
if !deleted_files.empty?
log 'deleted_files' do
message lazy { "Deleted Files: #{deleted_files}" }
end
end
if !updated_files.empty?
log 'updated_files' do
message lazy { "Updated Files: #{updated_files}" }
end
end
ruby_block 'updated_status' do
block do
node.normal['deleted_files'] = deleted_files
node.normal['updated_files'] = updated_files
node.normal['last_run_time'] = Time.now
end
end