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:
parent
73053eb5ef
commit
f085af4ba6
|
@ -4,6 +4,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
||||||
"github.com/hashicorp/terraform/repl"
|
"github.com/hashicorp/terraform/repl"
|
||||||
|
@ -95,9 +96,29 @@ func (c *ConsoleCommand) Run(args []string) int {
|
||||||
ErrorWriter: wrappedstreams.Stderr(),
|
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
|
// IO Loop
|
||||||
session := &repl.Session{
|
session := &repl.Session{
|
||||||
Interpolater: ctx.Interpolater(),
|
Scope: scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if stdin is a pipe. If so, we evaluate directly.
|
// 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
|
var lastResult string
|
||||||
scanner := bufio.NewScanner(wrappedstreams.Stdin())
|
scanner := bufio.NewScanner(wrappedstreams.Stdin())
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
// Handle it. If there is an error exit immediately
|
result, exit, diags := session.Handle(strings.TrimSpace(scanner.Text()))
|
||||||
result, err := session.Handle(strings.TrimSpace(scanner.Text()))
|
if diags.HasErrors() {
|
||||||
if err != nil {
|
// In piped mode we'll exit immediately on error.
|
||||||
ui.Error(err.Error())
|
c.showDiagnostics(diags)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if exit {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// Store the last result
|
// Store the last result
|
||||||
lastResult = result
|
lastResult = result
|
||||||
|
|
|
@ -45,13 +45,12 @@ func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := session.Handle(line)
|
out, exit, diags := session.Handle(line)
|
||||||
if err == repl.ErrSessionExit {
|
if diags.HasErrors() {
|
||||||
break
|
c.showDiagnostics(diags)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if exit {
|
||||||
ui.Error(err.Error())
|
break
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Output(out)
|
ui.Output(out)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,12 +18,26 @@ func FormatResult(value interface{}) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatResult(value interface{}, nested bool) (string, error) {
|
func formatResult(value interface{}, nested bool) (string, error) {
|
||||||
|
if value == nil {
|
||||||
|
return "null", nil
|
||||||
|
}
|
||||||
switch output := value.(type) {
|
switch output := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
if nested {
|
if nested {
|
||||||
return fmt.Sprintf("%q", output), nil
|
return fmt.Sprintf("%q", output), nil
|
||||||
}
|
}
|
||||||
return 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{}:
|
case []interface{}:
|
||||||
return formatListResult(output)
|
return formatListResult(output)
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
|
|
|
@ -5,8 +5,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/zclconf/go-cty/cty"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
|
"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
|
// 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.
|
// Session represents the state for a single REPL session.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
// Interpolater is used for calculating interpolations
|
// Scope is the evaluation scope where expressions will be evaluated.
|
||||||
Interpolater *terraform.Interpolater
|
Scope *lang.Scope
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle handles a single line of input from the REPL.
|
// 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.
|
// a variable). This function should not be called in parallel.
|
||||||
//
|
//
|
||||||
// The return value is the output and the error to show.
|
// 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 {
|
switch {
|
||||||
|
case strings.TrimSpace(line) == "":
|
||||||
|
return "", false, nil
|
||||||
case strings.TrimSpace(line) == "exit":
|
case strings.TrimSpace(line) == "exit":
|
||||||
return "", ErrSessionExit
|
return "", true, nil
|
||||||
case strings.TrimSpace(line) == "help":
|
case strings.TrimSpace(line) == "help":
|
||||||
return s.handleHelp()
|
ret, diags := s.handleHelp()
|
||||||
|
return ret, false, diags
|
||||||
default:
|
default:
|
||||||
return s.handleEval(line)
|
ret, diags := s.handleEval(line)
|
||||||
|
return ret, false, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) handleEval(line string) (string, error) {
|
func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) {
|
||||||
// Wrap the line to make it an interpolation.
|
var diags tfdiags.Diagnostics
|
||||||
line = fmt.Sprintf("${%s}", line)
|
|
||||||
|
|
||||||
// Parse the line
|
// Parse the given line as an expression
|
||||||
raw, err := config.NewRawConfig(map[string]interface{}{
|
expr, parseDiags := hclsyntax.ParseExpression([]byte(line), "<console-input>", hcl.Pos{Line: 1, Column: 1})
|
||||||
"value": line,
|
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 {
|
if err != nil {
|
||||||
return "", err
|
diags = diags.Append(err)
|
||||||
|
return "", diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the value
|
return result, diags
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) handleHelp() (string, error) {
|
func (s *Session) handleHelp() (string, tfdiags.Diagnostics) {
|
||||||
text := `
|
text := `
|
||||||
The Terraform console allows you to experiment with Terraform interpolations.
|
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
|
You may access resources in the state (if you have one) just as you would
|
||||||
|
|
Loading…
Reference in New Issue