#!/usr/bin/env ruby
# Script to concat files to a config file.
#
# Given a directory like this:
# /path/to/conf.d
# |-- fragments
# |   |-- 00_named.conf
# |   |-- 10_domain.net
# |   `-- zz_footer
#
# The script supports a test option that will build the concat file to a temp location and
# use /usr/bin/cmp to verify if it should be run or not.  This would result in the concat happening
# twice on each run but gives you the option to have an unless option in your execs to inhibit rebuilds.
#
# Without the test option and the unless combo your services that depend on the final file would end up
# restarting on each run, or in other manifest models some changes might get missed.
#
# OPTIONS:
#  -o	The file to create from the sources
#  -d	The directory where the fragments are kept
#  -t	Test to find out if a build is needed, basically concats the files to a temp
#       location and compare with what's in the final location, return codes are designed
#       for use with unless on an exec resource
#  -w   Add a shell style comment at the top of the created file to warn users that it
#       is generated by puppet
#  -f   Enables the creation of empty output files when no fragments are found
#  -n	Sort the output numerically rather than the default alpha sort
#
# the command:
#
#   concatfragments.rb -o /path/to/conffile.cfg -d /path/to/conf.d
#
# creates /path/to/conf.d/fragments.concat and copies the resulting
# file to /path/to/conffile.cfg.  The files will be sorted alphabetically
# pass the -n switch to sort numerically.
#
# The script does error checking on the various dirs and files to make
# sure things don't fail.
require 'optparse'
require 'fileutils'

settings = {
    :outfile => "",
    :workdir => "",
    :test => false,
    :force => false,
    :warn => "",
    :sortarg => "",
    :newline => false
}

OptionParser.new do |opts|
  opts.banner = "Usage: #{$0} [options]"
  opts.separator "Specific options:"

  opts.on("-o", "--outfile OUTFILE", String, "The file to create from the sources") do |o|
    settings[:outfile] = o
  end

  opts.on("-d", "--workdir WORKDIR", String, "The directory where the fragments are kept") do |d|
    settings[:workdir] = d
  end

  opts.on("-t", "--test", "Test to find out if a build is needed") do
    settings[:test] = true
  end

  opts.separator "Other options:"
  opts.on("-w", "--warn WARNMSG", String,
          "Add a shell style comment at the top of the created file to warn users that it is generated by puppet") do |w|
    settings[:warn] = w
  end

  opts.on("-f", "--force", "Enables the creation of empty output files when no fragments are found") do
    settings[:force] = true
  end

  opts.on("-n", "--sort", "Sort the output numerically rather than the default alpha sort") do
    settings[:sortarg] = "-n"
  end

  opts.on("-l", "--line", "Append a newline") do
    settings[:newline] = true
  end
end.parse!

# do we have -o?
raise 'Please specify an output file with -o' unless !settings[:outfile].empty?

# do we have -d?
raise 'Please specify fragments directory with -d' unless !settings[:workdir].empty?

# can we write to -o?
if File.file?(settings[:outfile])
  if !File.writable?(settings[:outfile])
    raise "Cannot write to #{settings[:outfile]}"
  end
else
  if !File.writable?(File.dirname(settings[:outfile]))
    raise "Cannot write to dirname #{File.dirname(settings[:outfile])} to create #{settings[:outfile]}"
  end
end

# do we have a fragments subdir inside the work dir?
if !File.directory?(File.join(settings[:workdir], "fragments")) && !File.executable?(File.join(settings[:workdir], "fragments"))
  raise "Cannot access the fragments directory"
end

# are there actually any fragments?
if (Dir.entries(File.join(settings[:workdir], "fragments")) - %w{ . .. }).empty?
  if !settings[:force]
    raise "The fragments directory is empty, cowardly refusing to make empty config files"
  end
end

Dir.chdir(settings[:workdir])

if settings[:warn].empty?
  File.open("fragments.concat", 'w') { |f| f.write("") }
else
  File.open("fragments.concat", 'w') { |f| f.write("#{settings[:warn]}\n") }
end

# find all the files in the fragments directory, sort them numerically and concat to fragments.concat in the working dir
open('fragments.concat', 'a') do |f|
  fragments = Dir.entries("fragments").sort
  if settings[:sortarg] == '-n'
    fragments = fragments.sort_by { |v| v.split('_').map(&:to_i) }
  end
  fragments.each { |entry|
    if File.file?(File.join("fragments", entry))
      f << File.read(File.join("fragments", entry))

      # append a newline if we were asked to (invoked with -l)
      if settings[:newline]
        f << "\n"
      end

    end
  }
end

if !settings[:test]
  # This is a real run, copy the file to outfile
  FileUtils.cp 'fragments.concat', settings[:outfile]
else
  # Just compare the result to outfile to help the exec decide
  if FileUtils.cmp 'fragments.concat', settings[:outfile]
    exit 0
  else
    exit 1
  end
end
