df48c6253c
Using `time` to calculate the average of 100 iterations on my machine, `packer` went from 130ms on average to 70ms. Previously, the load time would scale linearly about 30ms (on my machine) on average per new command added. Now that is much much smaller.
359 lines
8.7 KiB
Go
359 lines
8.7 KiB
Go
// The packer package contains the core components of Packer.
|
|
package packer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// The function type used to lookup Builder implementations.
|
|
type BuilderFunc func(name string) (Builder, error)
|
|
|
|
// The function type used to lookup Command implementations.
|
|
type CommandFunc func(name string) (Command, error)
|
|
|
|
// The function type used to lookup Hook implementations.
|
|
type HookFunc func(name string) (Hook, error)
|
|
|
|
// The function type used to lookup PostProcessor implementations.
|
|
type PostProcessorFunc func(name string) (PostProcessor, error)
|
|
|
|
// The function type used to lookup Provisioner implementations.
|
|
type ProvisionerFunc func(name string) (Provisioner, error)
|
|
|
|
// ComponentFinder is a struct that contains the various function
|
|
// pointers necessary to look up components of Packer such as builders,
|
|
// commands, etc.
|
|
type ComponentFinder struct {
|
|
Builder BuilderFunc
|
|
Command CommandFunc
|
|
Hook HookFunc
|
|
PostProcessor PostProcessorFunc
|
|
Provisioner ProvisionerFunc
|
|
}
|
|
|
|
// The environment interface provides access to the configuration and
|
|
// state of a single Packer run.
|
|
//
|
|
// It allows for things such as executing CLI commands, getting the
|
|
// list of available builders, and more.
|
|
type Environment interface {
|
|
Builder(string) (Builder, error)
|
|
Cache() Cache
|
|
Cli([]string) (int, error)
|
|
Hook(string) (Hook, error)
|
|
PostProcessor(string) (PostProcessor, error)
|
|
Provisioner(string) (Provisioner, error)
|
|
Ui() Ui
|
|
}
|
|
|
|
// An implementation of an Environment that represents the Packer core
|
|
// environment.
|
|
type coreEnvironment struct {
|
|
cache Cache
|
|
commands []string
|
|
components ComponentFinder
|
|
ui Ui
|
|
}
|
|
|
|
// This struct configures new environments.
|
|
type EnvironmentConfig struct {
|
|
Cache Cache
|
|
Commands []string
|
|
Components ComponentFinder
|
|
Ui Ui
|
|
}
|
|
|
|
type helpCommandEntry struct {
|
|
i int
|
|
key string
|
|
synopsis string
|
|
}
|
|
|
|
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
|
|
// be used to create a new enviroment with NewEnvironment with sane defaults.
|
|
func DefaultEnvironmentConfig() *EnvironmentConfig {
|
|
config := &EnvironmentConfig{}
|
|
config.Commands = make([]string, 0)
|
|
config.Ui = &BasicUi{
|
|
Reader: os.Stdin,
|
|
Writer: os.Stdout,
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// This creates a new environment
|
|
func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error) {
|
|
if config == nil {
|
|
err = errors.New("config must be given to initialize environment")
|
|
return
|
|
}
|
|
|
|
env := &coreEnvironment{}
|
|
env.cache = config.Cache
|
|
env.commands = config.Commands
|
|
env.components = config.Components
|
|
env.ui = config.Ui
|
|
|
|
// We want to make sure the components have valid function pointers.
|
|
// If a function pointer was not given, we assume that the function
|
|
// will just return a nil component.
|
|
if env.components.Builder == nil {
|
|
env.components.Builder = func(string) (Builder, error) { return nil, nil }
|
|
}
|
|
|
|
if env.components.Command == nil {
|
|
env.components.Command = func(string) (Command, error) { return nil, nil }
|
|
}
|
|
|
|
if env.components.Hook == nil {
|
|
env.components.Hook = func(string) (Hook, error) { return nil, nil }
|
|
}
|
|
|
|
if env.components.PostProcessor == nil {
|
|
env.components.PostProcessor = func(string) (PostProcessor, error) { return nil, nil }
|
|
}
|
|
|
|
if env.components.Provisioner == nil {
|
|
env.components.Provisioner = func(string) (Provisioner, error) { return nil, nil }
|
|
}
|
|
|
|
// The default cache is just the system temporary directory
|
|
if env.cache == nil {
|
|
env.cache = &FileCache{CacheDir: os.TempDir()}
|
|
}
|
|
|
|
resultEnv = env
|
|
return
|
|
}
|
|
|
|
// Returns a builder of the given name that is registered with this
|
|
// environment.
|
|
func (e *coreEnvironment) Builder(name string) (b Builder, err error) {
|
|
b, err = e.components.Builder(name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if b == nil {
|
|
err = fmt.Errorf("No builder returned for name: %s", name)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Returns the cache for this environment
|
|
func (e *coreEnvironment) Cache() Cache {
|
|
return e.cache
|
|
}
|
|
|
|
// Returns a hook of the given name that is registered with this
|
|
// environment.
|
|
func (e *coreEnvironment) Hook(name string) (h Hook, err error) {
|
|
h, err = e.components.Hook(name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if h == nil {
|
|
err = fmt.Errorf("No hook returned for name: %s", name)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Returns a PostProcessor for the given name that is registered with this
|
|
// environment.
|
|
func (e *coreEnvironment) PostProcessor(name string) (p PostProcessor, err error) {
|
|
p, err = e.components.PostProcessor(name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if p == nil {
|
|
err = fmt.Errorf("No post processor found for name: %s", name)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Returns a provisioner for the given name that is registered with this
|
|
// environment.
|
|
func (e *coreEnvironment) Provisioner(name string) (p Provisioner, err error) {
|
|
p, err = e.components.Provisioner(name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if p == nil {
|
|
err = fmt.Errorf("No provisioner returned for name: %s", name)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Executes a command as if it was typed on the command-line interface.
|
|
// The return value is the exit code of the command.
|
|
func (e *coreEnvironment) Cli(args []string) (result int, err error) {
|
|
log.Printf("Environment.Cli: %#v\n", args)
|
|
|
|
// If we have no arguments, just short-circuit here and print the help
|
|
if len(args) == 0 {
|
|
e.printHelp()
|
|
return 1, nil
|
|
}
|
|
|
|
// This variable will track whether or not we're supposed to print
|
|
// the help or not.
|
|
isHelp := false
|
|
for _, arg := range args {
|
|
if arg == "-h" || arg == "--help" {
|
|
isHelp = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Trim up to the command name
|
|
for i, v := range args {
|
|
if v[0] != '-' {
|
|
args = args[i:]
|
|
break
|
|
}
|
|
}
|
|
|
|
log.Printf("command + args: %#v", args)
|
|
|
|
version := args[0] == "version"
|
|
if !version {
|
|
for _, arg := range args {
|
|
if arg == "--version" || arg == "-v" {
|
|
version = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var command Command
|
|
if version {
|
|
command = new(versionCommand)
|
|
}
|
|
|
|
if command == nil {
|
|
command, err = e.components.Command(args[0])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// If we still don't have a command, show the help.
|
|
if command == nil {
|
|
e.ui.Error(fmt.Sprintf("Unknown command: %s\n", args[0]))
|
|
e.printHelp()
|
|
return 1, nil
|
|
}
|
|
}
|
|
|
|
// If we're supposed to print help, then print the help of the
|
|
// command rather than running it.
|
|
if isHelp {
|
|
e.ui.Say(command.Help())
|
|
return 0, nil
|
|
}
|
|
|
|
log.Printf("Executing command: %s\n", args[0])
|
|
return command.Run(e, args[1:]), nil
|
|
}
|
|
|
|
// Prints the CLI help to the UI.
|
|
func (e *coreEnvironment) printHelp() {
|
|
// Created a sorted slice of the map keys and record the longest
|
|
// command name so we can better format the output later.
|
|
maxKeyLen := 0
|
|
for _, command := range e.commands {
|
|
if len(command) > maxKeyLen {
|
|
maxKeyLen = len(command)
|
|
}
|
|
}
|
|
|
|
// Sort the keys
|
|
sort.Strings(e.commands)
|
|
|
|
// Create the communication/sync mechanisms to get the synopsis' of
|
|
// the various commands. We do this in parallel since the overhead
|
|
// of the subprocess underneath is very expensive and this speeds things
|
|
// up an incredible amount.
|
|
var wg sync.WaitGroup
|
|
ch := make(chan *helpCommandEntry)
|
|
|
|
for i, key := range e.commands {
|
|
wg.Add(1)
|
|
|
|
// Get the synopsis in a goroutine since it may take awhile
|
|
// to subprocess out.
|
|
go func(i int, key string) {
|
|
defer wg.Done()
|
|
var synopsis string
|
|
command, err := e.components.Command(key)
|
|
if err != nil {
|
|
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
|
|
} else if command == nil {
|
|
return
|
|
} else {
|
|
synopsis = command.Synopsis()
|
|
}
|
|
|
|
// Pad the key with spaces so that they're all the same width
|
|
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
|
|
|
// Output the command and the synopsis
|
|
ch <- &helpCommandEntry{
|
|
i: i,
|
|
key: key,
|
|
synopsis: synopsis,
|
|
}
|
|
}(i, key)
|
|
}
|
|
|
|
e.ui.Say("usage: packer [--version] [--help] <command> [<args>]\n")
|
|
e.ui.Say("Available commands are:")
|
|
|
|
// Make a goroutine that just waits for all the synopsis gathering
|
|
// to complete, and then output it.
|
|
synopsisDone := make(chan struct{})
|
|
go func() {
|
|
defer close(synopsisDone)
|
|
entries := make([]string, len(e.commands))
|
|
|
|
for entry := range ch {
|
|
e.ui.Machine("command", entry.key, entry.synopsis)
|
|
message := fmt.Sprintf(" %s %s", entry.key, entry.synopsis)
|
|
entries[entry.i] = message
|
|
}
|
|
|
|
for _, message := range entries {
|
|
if message != "" {
|
|
e.ui.Say(message)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait to complete getting the synopsis' then close the channel
|
|
wg.Wait()
|
|
close(ch)
|
|
<-synopsisDone
|
|
|
|
e.ui.Say("\nGlobally recognized options:")
|
|
e.ui.Say(" -machine-readable Machine-readable output format.")
|
|
}
|
|
|
|
// Returns the UI for the environment. The UI is the interface that should
|
|
// be used for all communication with the outside world.
|
|
func (e *coreEnvironment) Ui() Ui {
|
|
return e.ui
|
|
}
|