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.
This commit is contained in:
Martin Atkins 2018-05-03 20:44:55 -07:00
parent 73053eb5ef
commit f085af4ba6
4 changed files with 98 additions and 56 deletions

View File

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

View File

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

View File

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

View File

@ -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), "<console-input>", 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