mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
repl: package for TF REPL
This commit is contained in:
parent
1a8fbdc428
commit
d9c522173d
92
repl/format.go
Normal file
92
repl/format.go
Normal file
@ -0,0 +1,92 @@
|
||||
package repl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FormatResult formats the given result value for human-readable output.
|
||||
//
|
||||
// The value must currently be a string, list, map, and any nested values
|
||||
// with those same types.
|
||||
func FormatResult(value interface{}) (string, error) {
|
||||
return formatResult(value)
|
||||
}
|
||||
|
||||
func formatResult(value interface{}) (string, error) {
|
||||
switch output := value.(type) {
|
||||
case string:
|
||||
return output, nil
|
||||
case []interface{}:
|
||||
return formatListResult(output)
|
||||
case map[string]interface{}:
|
||||
return formatMapResult(output)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown value type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func formatListResult(value []interface{}) (string, error) {
|
||||
var outputBuf bytes.Buffer
|
||||
outputBuf.WriteString("[")
|
||||
if len(value) > 0 {
|
||||
outputBuf.WriteString("\n")
|
||||
}
|
||||
|
||||
lastIdx := len(value) - 1
|
||||
for i, v := range value {
|
||||
raw, err := formatResult(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
outputBuf.WriteString(indent(raw))
|
||||
if lastIdx != i {
|
||||
outputBuf.WriteString(",")
|
||||
}
|
||||
outputBuf.WriteString("\n")
|
||||
}
|
||||
|
||||
outputBuf.WriteString("]")
|
||||
return outputBuf.String(), nil
|
||||
}
|
||||
|
||||
func formatMapResult(value map[string]interface{}) (string, error) {
|
||||
ks := make([]string, 0, len(value))
|
||||
for k, _ := range value {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
sort.Strings(ks)
|
||||
|
||||
var outputBuf bytes.Buffer
|
||||
outputBuf.WriteString("{")
|
||||
if len(value) > 0 {
|
||||
outputBuf.WriteString("\n")
|
||||
}
|
||||
|
||||
for _, k := range ks {
|
||||
v := value[k]
|
||||
raw, err := formatResult(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
outputBuf.WriteString(indent(fmt.Sprintf("%s = %v\n", k, raw)))
|
||||
}
|
||||
|
||||
outputBuf.WriteString("}")
|
||||
return outputBuf.String(), nil
|
||||
}
|
||||
|
||||
func indent(value string) string {
|
||||
var outputBuf bytes.Buffer
|
||||
s := bufio.NewScanner(strings.NewReader(value))
|
||||
for s.Scan() {
|
||||
outputBuf.WriteString(" " + s.Text())
|
||||
}
|
||||
|
||||
return outputBuf.String()
|
||||
}
|
4
repl/repl.go
Normal file
4
repl/repl.go
Normal file
@ -0,0 +1,4 @@
|
||||
// Package repl provides the structs and functions necessary to run
|
||||
// REPL for Terraform. The REPL allows experimentation of Terraform
|
||||
// interpolations without having to run a Terraform configuration.
|
||||
package repl
|
95
repl/session.go
Normal file
95
repl/session.go
Normal file
@ -0,0 +1,95 @@
|
||||
package repl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// ErrSessionExit is a special error result that should be checked for
|
||||
// from Handle to signal a graceful exit.
|
||||
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
|
||||
}
|
||||
|
||||
// Handle handles a single line of input from the REPL.
|
||||
//
|
||||
// This is a stateful operation if a command is given (such as setting
|
||||
// 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) {
|
||||
switch {
|
||||
case strings.TrimSpace(line) == "exit":
|
||||
return "", ErrSessionExit
|
||||
case strings.TrimSpace(line) == "help":
|
||||
return s.handleHelp()
|
||||
default:
|
||||
return s.handleEval(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) handleEval(line string) (string, error) {
|
||||
// Wrap the line to make it an interpolation.
|
||||
line = fmt.Sprintf("${%s}", line)
|
||||
|
||||
// Parse the line
|
||||
raw, err := config.NewRawConfig(map[string]interface{}{
|
||||
"value": line,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s *Session) handleHelp() (string, error) {
|
||||
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
|
||||
from a configuration. For example: "aws_instance.foo.id" would evaluate
|
||||
to the ID of "aws_instance.foo" if it exists in your state.
|
||||
|
||||
Type in the interpolation to test and hit <enter> to see the result.
|
||||
|
||||
To exit the console, type "exit" and hit <enter>, or use Control-C or
|
||||
Control-D.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
193
repl/session_test.go
Normal file
193
repl/session_test.go
Normal file
@ -0,0 +1,193 @@
|
||||
package repl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestSession_basicState(t *testing.T) {
|
||||
state := &terraform.State{
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Resources: map[string]*terraform.ResourceState{
|
||||
"test_instance.foo": &terraform.ResourceState{
|
||||
Type: "test_instance",
|
||||
Primary: &terraform.InstanceState{
|
||||
ID: "bar",
|
||||
Attributes: map[string]string{
|
||||
"id": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
State: state,
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "test_instance.foo.id",
|
||||
Output: "bar",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("resource count", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
State: state,
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "test_instance.foo.count",
|
||||
Output: "1",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("missing resource", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
State: state,
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "test_instance.bar.id",
|
||||
Error: true,
|
||||
ErrorContains: "'test_instance.bar' not found",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_stateless(t *testing.T) {
|
||||
t.Run("exit", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "exit",
|
||||
Error: true,
|
||||
ErrorContains: ErrSessionExit.Error(),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("help", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "help",
|
||||
OutputContains: "allows you to",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("help with spaces", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "help ",
|
||||
OutputContains: "allows you to",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("basic math", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "1 + 5",
|
||||
Output: "6",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("missing resource", func(t *testing.T) {
|
||||
testSession(t, testSessionTest{
|
||||
Inputs: []testSessionInput{
|
||||
{
|
||||
Input: "test_instance.bar.id",
|
||||
Error: true,
|
||||
ErrorContains: "'test_instance.bar' not found",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testSession(t *testing.T, test testSessionTest) {
|
||||
// Build the TF context
|
||||
ctx, err := terraform.NewContext(&terraform.ContextOpts{
|
||||
State: test.State,
|
||||
Module: module.NewEmptyTree(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Build the session
|
||||
s := &Session{
|
||||
Interpolater: ctx.Interpolater(),
|
||||
}
|
||||
|
||||
// Test the inputs. We purposely don't use subtests here because
|
||||
// the inputs don't recognize subtests, but a sequence of stateful
|
||||
// operations.
|
||||
for _, input := range test.Inputs {
|
||||
result, err := s.Handle(input.Input)
|
||||
if (err != nil) != input.Error {
|
||||
t.Fatalf("%q: err: %s", input.Input, err)
|
||||
}
|
||||
if err != nil {
|
||||
if input.ErrorContains != "" {
|
||||
if !strings.Contains(err.Error(), input.ErrorContains) {
|
||||
t.Fatalf(
|
||||
"%q: err should contain: %q\n\n%s",
|
||||
input.Input, input.ErrorContains, err)
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if input.Output != "" && result != input.Output {
|
||||
t.Fatalf(
|
||||
"%q: expected:\n\n%s\n\ngot:\n\n%s",
|
||||
input.Input, input.Output, result)
|
||||
}
|
||||
|
||||
if input.OutputContains != "" && !strings.Contains(result, input.OutputContains) {
|
||||
t.Fatalf(
|
||||
"%q: expected contains:\n\n%s\n\ngot:\n\n%s",
|
||||
input.Input, input.OutputContains, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testSessionTest struct {
|
||||
State *terraform.State // State to use
|
||||
Module string // Module name in test-fixtures to load
|
||||
|
||||
// Inputs are the list of test inputs that are run in order.
|
||||
// Each input can test the output of each step.
|
||||
Inputs []testSessionInput
|
||||
}
|
||||
|
||||
// testSessionInput is a single input to test for a session.
|
||||
type testSessionInput struct {
|
||||
Input string // Input string
|
||||
Output string // Exact output string to check
|
||||
OutputContains string
|
||||
Error bool // Error is true if error is expected
|
||||
ErrorContains string
|
||||
}
|
Loading…
Reference in New Issue
Block a user