Skip to content

Commit

Permalink
Add packer/bin/bento for building templates.
Browse files Browse the repository at this point in the history
This is a first-step addition of a small binary wrapper around Packer to
help inject custom variables, inject metadata and re-normalize the
templates using `packer fix`.

In its present implementation, no external RubyGems are required in an
attempt to maximize portability between platforms.
  • Loading branch information
fnichol committed May 20, 2015
1 parent 0660bcc commit 3ca22ff
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 1 deletion.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
bin/
.bundle/
iso
*.box
Expand Down
226 changes: 226 additions & 0 deletions bin/bento
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env ruby
# -*- encoding: utf-8 -*-

Signal.trap("INT") { exit 1 }

$stdout.sync = true
$stderr.sync = true

require 'optparse'
require 'ostruct'
require 'benchmark'

class Options

NAME = File.basename($0).freeze

def self.parse(args)
options = OpenStruct.new
options.templates = calculate_templates("*.json")

ENV['PACKER_CACHE_DIR'] = "packer_cache"

global = OptionParser.new do |opts|
opts.banner = "Usage: #{NAME} [SUBCOMMAND [options]]"
opts.separator ""
opts.separator <<-COMMANDS.gsub(/^ {8}/, "")
build : build one or more templates
fix : fix one or more templates
help : prints this help message
list : list all templates in project
COMMANDS
end

templates_argv_proc = proc { |options|
options.templates = calculate_templates(args) unless args.empty?

options.templates.each do |t|
if !File.exists?("#{t}.json")
$stderr.puts "File #{t}.json does not exist for template '#{t}'"
exit(1)
end
end
}

subcommand = {
help: {
parser: OptionParser.new {},
argv: proc { |options|
puts global
exit(0)
}
},
build: {
class: BuildRunner,
parser: OptionParser.new { |opts|
opts.banner = "Usage: #{NAME} build [options] TEMPLATE[ TEMPLATE ...]"

opts.on("-n", "--[no-]dry-run", "Dry run (what would happen)") do |opt|
options.dry_run = opt
end

opts.on("-d", "--[no-]debug", "Run packer with debug output") do |opt|
options.debug = opt
end

opts.on("-o BUILDS", "--only BUILDS", "Only build some Packer builds") do |opt|
options.builds = opt
end
},
argv: templates_argv_proc
},
fix: {
class: FixRunner,
parser: OptionParser.new { |opts|
opts.banner = "Usage: #{NAME} fix TEMPLATE[ TEMPLATE ...]"
},
argv: templates_argv_proc
},
list: {
class: ListRunner,
parser: OptionParser.new { |opts|
opts.banner = "Usage: #{NAME} list [TEMPLATE ...]"
},
argv: templates_argv_proc
}
}

global.order!
command = args.empty? ? :help : ARGV.shift.to_sym
subcommand.fetch(command).fetch(:parser).order!
subcommand.fetch(command).fetch(:argv).call(options)

options.command = command
options.klass = subcommand.fetch(command).fetch(:class)

options
end

def self.calculate_templates(globs)
Array(globs).
map { |glob| result = Dir.glob(glob); result.empty? ? glob : result }.
flatten.
sort.
delete_if { |file| file =~ /\.variables\./ }.
map { |template| template.sub(/\.json$/, '') }
end
end

module Common

def banner(msg)
puts "==> #{msg}"
end

def duration(total)
total = 0 if total.nil?
minutes = (total / 60).to_i
seconds = (total - (minutes * 60))
format("%dm%.2fs", minutes, seconds)
end
end

class BuildRunner

include Common

attr_reader :templates, :dry_run, :debug, :builds

def initialize(opts)
@templates = opts.templates
@dry_run = opts.dry_run
@debug = opts.debug
@builds = opts.builds
end

def start
banner("Starting build for templates: #{templates}")
time = Benchmark.measure do
templates.each { |template| packer(template) }
end
banner("Build finished in #{duration(time.real)}.")
end

def packer(template)
cmd = packer_cmd(template)
banner("[#{template}] Running: '#{cmd.join(' ')}'")
time = Benchmark.measure do
system(*cmd) or raise "[#{template}] Error building, exited #{$?}"
end
banner("[#{template}] Finished in #{duration(time.real)}.")
end

def packer_cmd(template)
vars = "#{template}.variables.json"
cmd = %W[packer build #{template}.json]
cmd.insert(2, "-var-file=#{vars}") if File.exist?(vars)
cmd.insert(2, "-only=#{builds}") if builds
cmd.insert(2, "-debug") if debug
cmd.insert(0, "echo") if dry_run
cmd
end

def git_sha
%x{git rev-parse --short HEAD}.strip
end
end

class FixRunner

include Common

attr_reader :templates

def initialize(opts)
@templates = opts.templates
end

def start
banner("Fixing for templates: #{templates}")
time = Benchmark.measure do
templates.each { |template| fix(template) }
end
banner("Fixing finished in #{duration(time.real)}.")
end

def fix(template)
output = %x{packer fix #{template}.json}
raise "[#{template}] Error fixing, exited #{$?}" if $?.exitstatus != 0
File.open("#{template}.json", "wb") { |file| file.write(output) }
end
end

class ListRunner

include Common

attr_reader :templates

def initialize(opts)
@templates = opts.templates
end

def start
templates.each { |template| puts template }
end
end

class Runner

attr_reader :options

def initialize(options)
@options = options
end

def start
options.klass.new(options).start
end
end

begin
Runner.new(Options.parse(ARGV)).start
rescue => ex
$stderr.puts ">>> #{ex.message}"
exit(($? && $?.exitstatus) || 99)
end

0 comments on commit 3ca22ff

Please sign in to comment.