diff --git a/command/console.go b/command/console.go index 1c53ef659..3ddc26d57 100644 --- a/command/console.go +++ b/command/console.go @@ -4,6 +4,7 @@ import ( "bufio" "strings" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/helper/wrappedstreams" "github.com/hashicorp/terraform/repl" @@ -95,9 +96,29 @@ func (c *ConsoleCommand) Run(args []string) int { ErrorWriter: wrappedstreams.Stderr(), } + // Before we can evaluate expressions, we must compute and populate any + // derived values (input variables, local values, output values) + // that are not stored in the persistent state. + scope, scopeDiags := ctx.Eval(addrs.RootModuleInstance) + diags = diags.Append(scopeDiags) + if scope == nil { + // scope is nil if there are errors so bad that we can't even build a scope. + // Otherwise, we'll try to eval anyway. + c.showDiagnostics(diags) + return 1 + } + if diags.HasErrors() { + diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results.")) + } + + // Before we become interactive we'll show any diagnostics we encountered + // during initialization, and then afterwards the driver will manage any + // further diagnostics itself. + c.showDiagnostics(diags) + // IO Loop session := &repl.Session{ - Interpolater: ctx.Interpolater(), + Scope: scope, } // Determine if stdin is a pipe. If so, we evaluate directly. @@ -112,12 +133,15 @@ func (c *ConsoleCommand) modePiped(session *repl.Session, ui cli.Ui) int { var lastResult string scanner := bufio.NewScanner(wrappedstreams.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()) + result, exit, diags := session.Handle(strings.TrimSpace(scanner.Text())) + if diags.HasErrors() { + // In piped mode we'll exit immediately on error. + c.showDiagnostics(diags) return 1 } + if exit { + return 0 + } // Store the last result lastResult = result diff --git a/command/console_interactive.go b/command/console_interactive.go index f963528bc..f8261bb57 100644 --- a/command/console_interactive.go +++ b/command/console_interactive.go @@ -45,13 +45,12 @@ func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int { break } - out, err := session.Handle(line) - if err == repl.ErrSessionExit { - break + out, exit, diags := session.Handle(line) + if diags.HasErrors() { + c.showDiagnostics(diags) } - if err != nil { - ui.Error(err.Error()) - continue + if exit { + break } ui.Output(out) diff --git a/repl/format.go b/repl/format.go index cc10f7467..d8f95ea3d 100644 --- a/repl/format.go +++ b/repl/format.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "sort" + "strconv" "strings" ) @@ -17,12 +18,26 @@ func FormatResult(value interface{}) (string, error) { } func formatResult(value interface{}, nested bool) (string, error) { + if value == nil { + return "null", nil + } switch output := value.(type) { case string: if nested { return fmt.Sprintf("%q", output), nil } return output, nil + case int: + return strconv.Itoa(output), nil + case float64: + return fmt.Sprintf("%g", output), nil + case bool: + switch { + case output == true: + return "true", nil + default: + return "false", nil + } case []interface{}: return formatListResult(output) case map[string]interface{}: diff --git a/repl/session.go b/repl/session.go index ef0cea15f..56e11b39d 100644 --- a/repl/session.go +++ b/repl/session.go @@ -5,8 +5,13 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/terraform" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/tfdiags" ) // ErrSessionExit is a special error result that should be checked for @@ -15,8 +20,8 @@ var ErrSessionExit = errors.New("session exit") // Session represents the state for a single REPL session. type Session struct { - // Interpolater is used for calculating interpolations - Interpolater *terraform.Interpolater + // Scope is the evaluation scope where expressions will be evaluated. + Scope *lang.Scope } // Handle handles a single line of input from the REPL. @@ -25,60 +30,59 @@ type Session struct { // a variable). This function should not be called in parallel. // // The return value is the output and the error to show. -func (s *Session) Handle(line string) (string, error) { +func (s *Session) Handle(line string) (string, bool, tfdiags.Diagnostics) { switch { + case strings.TrimSpace(line) == "": + return "", false, nil case strings.TrimSpace(line) == "exit": - return "", ErrSessionExit + return "", true, nil case strings.TrimSpace(line) == "help": - return s.handleHelp() + ret, diags := s.handleHelp() + return ret, false, diags default: - return s.handleEval(line) + ret, diags := s.handleEval(line) + return ret, false, diags } } -func (s *Session) handleEval(line string) (string, error) { - // Wrap the line to make it an interpolation. - line = fmt.Sprintf("${%s}", line) +func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // Parse the line - raw, err := config.NewRawConfig(map[string]interface{}{ - "value": line, - }) + // Parse the given line as an expression + expr, parseDiags := hclsyntax.ParseExpression([]byte(line), "", hcl.Pos{Line: 1, Column: 1}) + diags = diags.Append(parseDiags) + if parseDiags.HasErrors() { + return "", diags + } + + val, valDiags := s.Scope.EvalExpr(expr, cty.DynamicPseudoType) + diags = diags.Append(valDiags) + if valDiags.HasErrors() { + return "", diags + } + + if !val.IsWhollyKnown() { + // FIXME: In future, once we've updated the result formatter to be + // cty-aware, we should just include unknown values as "(not yet known)" + // in the serialized result, allowing the rest (if any) to be seen. + diags = diags.Append(fmt.Errorf("Result depends on values that cannot be determined until after \"terraform apply\".")) + return "", diags + } + + // Our formatter still wants an old-style raw interface{} value, so + // for now we'll just shim it. + // FIXME: Port the formatter to work with cty.Value directly. + legacyVal := hcl2shim.ConfigValueFromHCL2(val) + result, err := FormatResult(legacyVal) if err != nil { - return "", err + diags = diags.Append(err) + return "", diags } - // Set the value - raw.Key = "value" - - // Get the values - vars, err := s.Interpolater.Values(&terraform.InterpolationScope{ - Path: []string{"root"}, - }, raw.Variables) - if err != nil { - return "", err - } - - // Interpolate - if err := raw.Interpolate(vars); err != nil { - return "", err - } - - // If we have any unknown keys, let the user know. - if ks := raw.UnknownKeys(); len(ks) > 0 { - return "", fmt.Errorf("unknown values referenced, can't compute value") - } - - // Read the value - result, err := FormatResult(raw.Value()) - if err != nil { - return "", err - } - - return result, nil + return result, diags } -func (s *Session) handleHelp() (string, error) { +func (s *Session) handleHelp() (string, tfdiags.Diagnostics) { text := ` The Terraform console allows you to experiment with Terraform interpolations. You may access resources in the state (if you have one) just as you would