From b8ac1a800d254a22d539b8fda606bd821890d1ad Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 4 Jun 2019 14:17:50 -0700 Subject: [PATCH] implement a packer console analogous to the terraform console --- command/console.go | 176 +++++++++++++++++++++++++++++++++++++++++++++ commands.go | 5 ++ packer/core.go | 1 - packer/ui.go | 6 +- 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 command/console.go diff --git a/command/console.go b/command/console.go new file mode 100644 index 000000000..e48744b49 --- /dev/null +++ b/command/console.go @@ -0,0 +1,176 @@ +package command + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template" + "github.com/hashicorp/packer/template/interpolate" + "github.com/posener/complete" +) + +const TiniestBuilder = `{ + "builders": [ + { + "type":"null", + "communicator": "none" + } + ] +}` + +type ConsoleCommand struct { + Meta +} + +func (c *ConsoleCommand) Run(args []string) int { + flags := c.Meta.FlagSet("console", FlagSetVars) + flags.Usage = func() { c.Ui.Say(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + var templ *template.Template + + args = flags.Args() + if len(args) < 1 { + // If user has not definied a builder, create a tiny null placeholder + // builder so that we can properly initialize the core + tpl, err := template.Parse(strings.NewReader(TiniestBuilder)) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to generate placeholder template: %s", err)) + return 1 + } + templ = tpl + } else if len(args) == 1 { + // Parse the provided template + tpl, err := template.ParseFile(args[0]) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err)) + return 1 + } + templ = tpl + } else { + // User provided too many arguments + flags.Usage() + return 1 + } + + // Get the core + core, err := c.Meta.Core(templ) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // IO Loop + session := &REPLSession{ + Core: core, + } + + return c.modeInteractive(session) +} + +func (*ConsoleCommand) Help() string { + helpText := ` +Usage: packer console [options] [TEMPLATE] + + Creates a console for testing variable interpolation. + If a template is provided, this command will load the template and any + variables defined therein into its context to be referenced during + interpolation. + +Options: + -var 'key=value' Variable for templates, can be used multiple times. + -var-file=path JSON file containing user variables. +` + + return strings.TrimSpace(helpText) +} + +func (*ConsoleCommand) Synopsis() string { + return "check that a template is valid" +} + +func (*ConsoleCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (*ConsoleCommand) AutocompleteFlags() complete.Flags { + return complete.Flags{ + "-var": complete.PredictNothing, + "-var-file": complete.PredictNothing, + } +} + +func (c *ConsoleCommand) modeInteractive(session *REPLSession) int { + for { + // Read a line + line, err := c.Ui.Ask("> ") + if err == packer.ErrInterrupted { + break + } else if err == io.EOF { + break + } + out, err := session.Handle(line) + if err == ErrSessionExit { + break + } + if err != nil { + c.Ui.Error(err.Error()) + continue + } + + c.Ui.Say(out) + } + + return 0 +} + +// ErrSessionExit is a special error result that should be checked for +// from Handle to signal a graceful exit. +var ErrSessionExit = errors.New("Session exit") + +// Session represents the state for a single REPL session. +type REPLSession struct { + // Core is used for constructing interpolations based off packer templates + Core *packer.Core +} + +// Handle handles a single line of input from the REPL. +// +// The return value is the output and the error to show. +func (s *REPLSession) Handle(line string) (string, error) { + switch { + case strings.TrimSpace(line) == "exit": + return "", ErrSessionExit + case strings.TrimSpace(line) == "help": + return s.handleHelp() + default: + return s.handleEval(line) + } +} + +func (s *REPLSession) handleEval(line string) (string, error) { + ctx := s.Core.Context() + rendered, err := interpolate.Render(line, ctx) + if err != nil { + return "", fmt.Errorf("Error interpolating: %s", err) + } + return rendered, nil +} + +func (s *REPLSession) handleHelp() (string, error) { + text := ` +The Packer console allows you to experiment with Packer interpolations. +You may access variables in the Packer config you called the console with. + +Type in the interpolation to test and hit to see the result. + +To exit the console, type "exit" and hit , or use Control-C. +` + + return strings.TrimSpace(text), nil +} diff --git a/commands.go b/commands.go index aff2ec9f8..f7533588c 100644 --- a/commands.go +++ b/commands.go @@ -22,6 +22,11 @@ func init() { Meta: *CommandMeta, }, nil }, + "console": func() (cli.Command, error) { + return &command.ConsoleCommand{ + Meta: *CommandMeta, + }, nil + }, "fix": func() (cli.Command, error) { return &command.FixCommand{ diff --git a/packer/core.go b/packer/core.go index 0b3670371..281d1c9c0 100644 --- a/packer/core.go +++ b/packer/core.go @@ -97,7 +97,6 @@ func NewCore(c *CoreConfig) (*Core, error) { result.builds[v] = b } - return result, nil } diff --git a/packer/ui.go b/packer/ui.go index 2ee866d2b..f2e25c050 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -18,6 +18,8 @@ import ( getter "github.com/hashicorp/go-getter" ) +var ErrInterrupted = errors.New("interrupted") + type UiColor uint const ( @@ -190,7 +192,7 @@ func (rw *BasicUi) Ask(query string) (string, error) { defer rw.l.Unlock() if rw.interrupted { - return "", errors.New("interrupted") + return "", ErrInterrupted } if rw.TTY == nil { @@ -228,7 +230,7 @@ func (rw *BasicUi) Ask(query string) (string, error) { // Mark that we were interrupted so future Ask calls fail. rw.interrupted = true - return "", errors.New("interrupted") + return "", ErrInterrupted } }