forked from hashicorp/terraform
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |