implement a packer console analogous to the terraform console

This commit is contained in:
Megan Marsh 2019-06-04 14:17:50 -07:00
parent 2620e18247
commit b8ac1a800d
4 changed files with 185 additions and 3 deletions

176
command/console.go Normal file
View File

@ -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 <enter> to see the result.
To exit the console, type "exit" and hit <enter>, or use Control-C.
`
return strings.TrimSpace(text), nil
}

View File

@ -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{

View File

@ -97,7 +97,6 @@ func NewCore(c *CoreConfig) (*Core, error) {
result.builds[v] = b
}
return result, nil
}

View File

@ -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
}
}