From f085af4ba60af926fc731736017779207b48430b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 3 May 2018 20:44:55 -0700 Subject: [PATCH] command: update "terraform console" for HCL2 This now uses the HCL2 parser and evaluator APIs and evaluates in terms of a new-style *lang.Scope, rather than the old terraform.Interpolator type that is no longer functional. The Context.Eval method used here behaves differently than the Context.Interpolater method used previously: it performs a graph walk to populate transient values such as input variables, local values, and output values, and produces its scope in terms of the result of that graph walk. Because of this, it is a lot more robust than the prior method when asked to resolve references other than those that are persisted in the state. --- command/console.go | 34 ++++++++++-- command/console_interactive.go | 11 ++-- repl/format.go | 15 ++++++ repl/session.go | 94 ++++++++++++++++++---------------- 4 files changed, 98 insertions(+), 56 deletions(-) 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