diff --git a/command/console.go b/command/console.go new file mode 100644 index 000000000..c18ff1bc2 --- /dev/null +++ b/command/console.go @@ -0,0 +1,183 @@ +package command + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/hashicorp/terraform/helper/wrappedreadline" + "github.com/hashicorp/terraform/repl" + + "github.com/chzyer/readline" + "github.com/mitchellh/cli" +) + +// ConsoleCommand is a Command implementation that applies a Terraform +// configuration and actually builds or changes infrastructure. +type ConsoleCommand struct { + Meta + + // When this channel is closed, the apply will be cancelled. + ShutdownCh <-chan struct{} +} + +func (c *ConsoleCommand) Run(args []string) int { + args = c.Meta.process(args, true) + cmdFlags := c.Meta.flagSet("console") + cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + pwd, err := os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + return 1 + } + + var configPath string + args = cmdFlags.Args() + if len(args) > 1 { + c.Ui.Error("The console command expects at most one argument.") + cmdFlags.Usage() + return 1 + } else if len(args) == 1 { + configPath = args[0] + } else { + configPath = pwd + } + + // Build the context based on the arguments given + ctx, _, err := c.Context(contextOpts{ + Path: configPath, + PathEmptyOk: true, + StatePath: c.Meta.statePath, + }) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // Setup the UI so we can output directly to stdout + ui := &cli.BasicUi{ + Writer: c.Stdout(), + ErrorWriter: c.Stderr(), + } + + // IO Loop + session := &repl.Session{ + Interpolater: ctx.Interpolater(), + } + + // Determine if stdin is a pipe. If so, we evaluate directly. + if c.StdinPiped() { + return c.modePiped(session, ui) + } + + return c.modeInteractive(session, ui) +} + +func (c *ConsoleCommand) modePiped(session *repl.Session, ui cli.Ui) int { + var lastResult string + scanner := bufio.NewScanner(c.Stdin()) + for scanner.Scan() { + // Handle it. If there is an error exit immediately + result, err := session.Handle(strings.TrimSpace(scanner.Text())) + if err != nil { + ui.Error(err.Error()) + return 1 + } + + // Store the last result + lastResult = result + } + + // Output the final result + ui.Output(lastResult) + + return 0 +} + +func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int { + // Configure input + l, err := readline.NewEx(wrappedreadline.Override(&readline.Config{ + Prompt: "> ", + InterruptPrompt: "^C", + EOFPrompt: "exit", + HistorySearchFold: true, + })) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error initializing console: %s", + err)) + return 1 + } + defer l.Close() + + for { + // Read a line + line, err := l.Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + + out, err := session.Handle(line) + if err == repl.ErrSessionExit { + break + } + if err != nil { + ui.Error(err.Error()) + continue + } + + ui.Output(out) + } + + return 0 +} + +func (c *ConsoleCommand) Help() string { + helpText := ` +Usage: terraform console [options] [DIR] + + Starts an interactive console for experimenting with Terraform + interpolations. + + This will open an interactive console that you can use to type + interpolations into and inspect their values. This command loads the + current state. This lets you explore and test interpolations before + using them in future configurations. + + This command will never modify your state. + + DIR can be set to a directory with a Terraform state to load. By + default, this will default to the current working directory. + +Options: + + -state=path Path to read state. Defaults to "terraform.tfstate" + + -var 'foo=bar' Set a variable in the Terraform configuration. This + flag can be set multiple times. + + -var-file=foo Set variables in the Terraform configuration from + a file. If "terraform.tfvars" is present, it will be + automatically loaded if this flag is not specified. + + +` + return strings.TrimSpace(helpText) +} + +func (c *ConsoleCommand) Synopsis() string { + return "Interactive console for Terraform interpolations" +} diff --git a/command/console_test.go b/command/console_test.go new file mode 100644 index 000000000..184bc7d0f --- /dev/null +++ b/command/console_test.go @@ -0,0 +1,6 @@ +package command + +// ConsoleCommand is tested primarily with tests in the "repl" package. +// It is not tested here because the Console uses a readline-like library +// that takes over stdin/stdout. It is difficult to test directly. The +// core logic is tested in "repl" diff --git a/command/meta.go b/command/meta.go index b2416abd5..8d434ba59 100644 --- a/command/meta.go +++ b/command/meta.go @@ -15,10 +15,12 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/helper/experiment" + "github.com/hashicorp/terraform/helper/wrappedreadline" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/mitchellh/panicwrap" ) // Meta are the meta-options that are available on all or most commands. @@ -310,6 +312,47 @@ func (m *Meta) Input() bool { return !test && m.input && len(m.variables) == 0 } +// StdinPiped returns true if the input is piped. +func (m *Meta) StdinPiped() bool { + fi, err := m.Stdin().Stat() + if err != nil { + // If there is an error, let's just say its not piped + return false + } + + return fi.Mode()&os.ModeNamedPipe != 0 +} + +// Stdin returns the stdin for this command. +func (m *Meta) Stdin() *os.File { + stdin := os.Stdin + if panicwrap.Wrapped(nil) { + stdin = wrappedreadline.Stdin + } + + return stdin +} + +// Stdout returns the stdout for this command. +func (m *Meta) Stdout() *os.File { + stdout := os.Stdout + if panicwrap.Wrapped(nil) { + stdout = wrappedreadline.Stdout + } + + return stdout +} + +// Stderr returns the stderr for this command. +func (m *Meta) Stderr() *os.File { + stderr := os.Stderr + if panicwrap.Wrapped(nil) { + stderr = wrappedreadline.Stderr + } + + return stderr +} + // contextOpts returns the options to use to initialize a Terraform // context with the settings from this Meta. func (m *Meta) contextOpts() *terraform.ContextOpts { diff --git a/commands.go b/commands.go index 13e05ab6e..dccacab2a 100644 --- a/commands.go +++ b/commands.go @@ -47,6 +47,13 @@ func init() { }, nil }, + "console": func() (cli.Command, error) { + return &command.ConsoleCommand{ + Meta: meta, + ShutdownCh: makeShutdownCh(), + }, nil + }, + "destroy": func() (cli.Command, error) { return &command.ApplyCommand{ Meta: meta, diff --git a/helper/wrappedreadline/wrappedreadline.go b/helper/wrappedreadline/wrappedreadline.go index 08314a1f3..ba54e1271 100644 --- a/helper/wrappedreadline/wrappedreadline.go +++ b/helper/wrappedreadline/wrappedreadline.go @@ -4,6 +4,9 @@ // panicwrap overrides the standard file descriptors so that the child process // no longer looks like a TTY. The helpers here access the extra file descriptors // passed by panicwrap to fix that. +// +// panicwrap should be checked for with panicwrap.Wrapped before using this +// librar, since this library won't adapt if the binary is not wrapped. package wrappedreadline import (