Merge branch 'master' into f-validate-list-no-duplicates

This commit is contained in:
Chris Marchesi 2017-08-28 13:45:16 -07:00 committed by GitHub
commit f17e14f576
39 changed files with 226 additions and 2400 deletions

View File

@ -2,8 +2,7 @@ dist: trusty
sudo: false sudo: false
language: go language: go
go: go:
- 1.8.3 - 1.9
- 1.9rc1
# add TF_CONSUL_TEST=1 to run consul tests # add TF_CONSUL_TEST=1 to run consul tests
# they were causing timouts in travis # they were causing timouts in travis

View File

@ -22,6 +22,11 @@ IMPROVEMENTS:
* backend/consul: can now set the path to a specific CA certificate file, client certificate file, and client key file that will be used when configuring the underlying Consul client. [GH-15405] * backend/consul: can now set the path to a specific CA certificate file, client certificate file, and client key file that will be used when configuring the underlying Consul client. [GH-15405]
* backend/http: now has optional support for locking, with special support from the target server. Additionally, the update operation can now optionally be implemented via `PUT` rather than `POST`. [GH-15793] * backend/http: now has optional support for locking, with special support from the target server. Additionally, the update operation can now optionally be implemented via `PUT` rather than `POST`. [GH-15793]
BUG FIXES:
* cli: `terraform init` now verifies the required Terraform version from the root module config. Previously this was verified only on subsequent commands, after initialization. [GH-15935]
* cli: `terraform validate` now consults `terraform.tfvars`, if present, to set variable values. This is now consistent with the behavior of other commands. [GH-15938]
## 0.10.2 (August 16, 2017) ## 0.10.2 (August 16, 2017)
BUG FIXES: BUG FIXES:

View File

@ -1,4 +1,4 @@
TEST?=$$(go list ./... | grep -v '/terraform/vendor/' | grep -v '/builtin/bins/') TEST?=./...
GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)
default: test vet default: test vet
@ -27,10 +27,11 @@ plugin-dev: generate
mv $(GOPATH)/bin/$(PLUGIN) $(GOPATH)/bin/terraform-$(PLUGIN) mv $(GOPATH)/bin/$(PLUGIN) $(GOPATH)/bin/terraform-$(PLUGIN)
# test runs the unit tests # test runs the unit tests
# we run this one package at a time here because running the entire suite in
# one command creates memory usage issues when running in Travis-CI.
test: fmtcheck generate test: fmtcheck generate
go test -i $(TEST) || exit 1 go test -i $(TEST) || exit 1
echo $(TEST) | \ go list $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=60s -parallel=4
xargs -t -n4 go test $(TESTARGS) -timeout=60s -parallel=4
# testacc runs acceptance tests # testacc runs acceptance tests
testacc: fmtcheck generate testacc: fmtcheck generate
@ -64,8 +65,8 @@ cover:
# vet runs the Go source code static analysis tool `vet` to find # vet runs the Go source code static analysis tool `vet` to find
# any common errors. # any common errors.
vet: vet:
@echo 'go vet $$(go list ./... | grep -v /terraform/vendor/)' @echo 'go vet ./...'
@go vet $$(go list ./... | grep -v /terraform/vendor/) ; if [ $$? -eq 1 ]; then \ @go vet ./... ; if [ $$? -eq 1 ]; then \
echo ""; \ echo ""; \
echo "Vet found suspicious constructs. Please check the reported constructs"; \ echo "Vet found suspicious constructs. Please check the reported constructs"; \
echo "and fix them if necessary before submitting the code for review."; \ echo "and fix them if necessary before submitting the code for review."; \
@ -78,7 +79,7 @@ generate:
@which stringer > /dev/null; if [ $$? -ne 0 ]; then \ @which stringer > /dev/null; if [ $$? -ne 0 ]; then \
go get -u golang.org/x/tools/cmd/stringer; \ go get -u golang.org/x/tools/cmd/stringer; \
fi fi
go generate $$(go list ./... | grep -v /terraform/vendor/) go generate ./...
@go fmt command/internal_plugin_list.go > /dev/null @go fmt command/internal_plugin_list.go > /dev/null
fmt: fmt:

View File

@ -35,7 +35,7 @@ All documentation is available on the [Terraform website](http://www.terraform.i
Developing Terraform Developing Terraform
-------------------- --------------------
If you wish to work on Terraform itself or any of its built-in providers, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.8+ is *required*). Alternatively, you can use the Vagrantfile in the root of this repo to stand up a virtual machine with the appropriate dev tooling already set up for you. If you wish to work on Terraform itself or any of its built-in providers, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.9+ is *required*). Alternatively, you can use the Vagrantfile in the root of this repo to stand up a virtual machine with the appropriate dev tooling already set up for you.
This repository contains only Terraform core, which includes the command line interface and the main graph engine. Providers are implemented as plugins that each have their own repository in [the `terraform-providers` organization](https://github.com/terraform-providers) on GitHub. Instructions for developing each provider are in the associated README file. For more information, see [the provider development overview](https://www.terraform.io/docs/plugins/provider.html). This repository contains only Terraform core, which includes the command line interface and the main graph engine. Providers are implemented as plugins that each have their own repository in [the `terraform-providers` organization](https://github.com/terraform-providers) on GitHub. Instructions for developing each provider are in the associated README file. For more information, see [the provider development overview](https://www.terraform.io/docs/plugins/provider.html).

2
Vagrantfile vendored
View File

@ -5,7 +5,7 @@
VAGRANTFILE_API_VERSION = "2" VAGRANTFILE_API_VERSION = "2"
# Software version variables # Software version variables
GOVERSION = "1.8.3" GOVERSION = "1.9"
UBUNTUVERSION = "16.04" UBUNTUVERSION = "16.04"
# CPU and RAM can be adjusted depending on your system # CPU and RAM can be adjusted depending on your system

View File

@ -13,6 +13,8 @@ import (
// TestBackendConfig validates and configures the backend with the // TestBackendConfig validates and configures the backend with the
// given configuration. // given configuration.
func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backend { func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backend {
t.Helper()
// Get the proper config structure // Get the proper config structure
rc, err := config.NewRawConfig(c) rc, err := config.NewRawConfig(c)
if err != nil { if err != nil {
@ -45,6 +47,8 @@ func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backen
// If you want to test locking, two backends must be given. If b2 is nil, // If you want to test locking, two backends must be given. If b2 is nil,
// then state lockign won't be tested. // then state lockign won't be tested.
func TestBackend(t *testing.T, b1, b2 Backend) { func TestBackend(t *testing.T, b1, b2 Backend) {
t.Helper()
testBackendStates(t, b1) testBackendStates(t, b1)
if b2 != nil { if b2 != nil {
@ -53,6 +57,8 @@ func TestBackend(t *testing.T, b1, b2 Backend) {
} }
func testBackendStates(t *testing.T, b Backend) { func testBackendStates(t *testing.T, b Backend) {
t.Helper()
states, err := b.States() states, err := b.States()
if err == ErrNamedStatesNotSupported { if err == ErrNamedStatesNotSupported {
t.Logf("TestBackend: named states not supported in %T, skipping", b) t.Logf("TestBackend: named states not supported in %T, skipping", b)
@ -231,6 +237,8 @@ func testBackendStates(t *testing.T, b Backend) {
} }
func testBackendStateLock(t *testing.T, b1, b2 Backend) { func testBackendStateLock(t *testing.T, b1, b2 Backend) {
t.Helper()
// Get the default state for each // Get the default state for each
b1StateMgr, err := b1.State(DefaultStateName) b1StateMgr, err := b1.State(DefaultStateName)
if err != nil { if err != nil {

View File

@ -54,6 +54,8 @@ func TestMain(m *testing.M) {
} }
func tempDir(t *testing.T) string { func tempDir(t *testing.T) string {
t.Helper()
dir, err := ioutil.TempDir("", "tf") dir, err := ioutil.TempDir("", "tf")
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -99,6 +101,8 @@ func metaOverridesForProviderAndProvisioner(p terraform.ResourceProvider, pr ter
} }
func testModule(t *testing.T, name string) *module.Tree { func testModule(t *testing.T, name string) *module.Tree {
t.Helper()
mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name)) mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name))
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -114,6 +118,8 @@ func testModule(t *testing.T, name string) *module.Tree {
// testPlan returns a non-nil noop plan. // testPlan returns a non-nil noop plan.
func testPlan(t *testing.T) *terraform.Plan { func testPlan(t *testing.T) *terraform.Plan {
t.Helper()
state := terraform.NewState() state := terraform.NewState()
state.RootModule().Outputs["foo"] = &terraform.OutputState{ state.RootModule().Outputs["foo"] = &terraform.OutputState{
Type: "string", Type: "string",
@ -127,6 +133,8 @@ func testPlan(t *testing.T) *terraform.Plan {
} }
func testPlanFile(t *testing.T, plan *terraform.Plan) string { func testPlanFile(t *testing.T, plan *terraform.Plan) string {
t.Helper()
path := testTempFile(t) path := testTempFile(t)
f, err := os.Create(path) f, err := os.Create(path)
@ -143,6 +151,8 @@ func testPlanFile(t *testing.T, plan *terraform.Plan) string {
} }
func testReadPlan(t *testing.T, path string) *terraform.Plan { func testReadPlan(t *testing.T, path string) *terraform.Plan {
t.Helper()
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -180,6 +190,8 @@ func testState() *terraform.State {
} }
func testStateFile(t *testing.T, s *terraform.State) string { func testStateFile(t *testing.T, s *terraform.State) string {
t.Helper()
path := testTempFile(t) path := testTempFile(t)
f, err := os.Create(path) f, err := os.Create(path)
@ -198,6 +210,8 @@ func testStateFile(t *testing.T, s *terraform.State) string {
// testStateFileDefault writes the state out to the default statefile // testStateFileDefault writes the state out to the default statefile
// in the cwd. Use `testCwd` to change into a temp cwd. // in the cwd. Use `testCwd` to change into a temp cwd.
func testStateFileDefault(t *testing.T, s *terraform.State) string { func testStateFileDefault(t *testing.T, s *terraform.State) string {
t.Helper()
f, err := os.Create(DefaultStateFilename) f, err := os.Create(DefaultStateFilename)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -214,6 +228,8 @@ func testStateFileDefault(t *testing.T, s *terraform.State) string {
// testStateFileRemote writes the state out to the remote statefile // testStateFileRemote writes the state out to the remote statefile
// in the cwd. Use `testCwd` to change into a temp cwd. // in the cwd. Use `testCwd` to change into a temp cwd.
func testStateFileRemote(t *testing.T, s *terraform.State) string { func testStateFileRemote(t *testing.T, s *terraform.State) string {
t.Helper()
path := filepath.Join(DefaultDataDir, DefaultStateFilename) path := filepath.Join(DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -234,6 +250,8 @@ func testStateFileRemote(t *testing.T, s *terraform.State) string {
// testStateRead reads the state from a file // testStateRead reads the state from a file
func testStateRead(t *testing.T, path string) *terraform.State { func testStateRead(t *testing.T, path string) *terraform.State {
t.Helper()
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -251,6 +269,8 @@ func testStateRead(t *testing.T, path string) *terraform.State {
// testStateOutput tests that the state at the given path contains // testStateOutput tests that the state at the given path contains
// the expected state string. // the expected state string.
func testStateOutput(t *testing.T, path string, expected string) { func testStateOutput(t *testing.T, path string, expected string) {
t.Helper()
newState := testStateRead(t, path) newState := testStateRead(t, path)
actual := strings.TrimSpace(newState.String()) actual := strings.TrimSpace(newState.String())
expected = strings.TrimSpace(expected) expected = strings.TrimSpace(expected)
@ -277,10 +297,14 @@ func testProvider() *terraform.MockResourceProvider {
} }
func testTempFile(t *testing.T) string { func testTempFile(t *testing.T) string {
t.Helper()
return filepath.Join(testTempDir(t), "state.tfstate") return filepath.Join(testTempDir(t), "state.tfstate")
} }
func testTempDir(t *testing.T) string { func testTempDir(t *testing.T) string {
t.Helper()
d, err := ioutil.TempDir("", "tf") d, err := ioutil.TempDir("", "tf")
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -292,6 +316,8 @@ func testTempDir(t *testing.T) string {
// testRename renames the path to new and returns a function to defer to // testRename renames the path to new and returns a function to defer to
// revert the rename. // revert the rename.
func testRename(t *testing.T, base, path, new string) func() { func testRename(t *testing.T, base, path, new string) func() {
t.Helper()
if base != "" { if base != "" {
path = filepath.Join(base, path) path = filepath.Join(base, path)
new = filepath.Join(base, new) new = filepath.Join(base, new)
@ -310,6 +336,8 @@ func testRename(t *testing.T, base, path, new string) func() {
// testChdir changes the directory and returns a function to defer to // testChdir changes the directory and returns a function to defer to
// revert the old cwd. // revert the old cwd.
func testChdir(t *testing.T, new string) func() { func testChdir(t *testing.T, new string) func() {
t.Helper()
old, err := os.Getwd() old, err := os.Getwd()
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -328,6 +356,8 @@ func testChdir(t *testing.T, new string) func() {
// testCwd is used to change the current working directory // testCwd is used to change the current working directory
// into a test directory that should be remoted after // into a test directory that should be remoted after
func testCwd(t *testing.T) (string, string) { func testCwd(t *testing.T) (string, string) {
t.Helper()
tmp, err := ioutil.TempDir("", "tf") tmp, err := ioutil.TempDir("", "tf")
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
@ -347,6 +377,8 @@ func testCwd(t *testing.T) (string, string) {
// testFixCwd is used to as a defer to testDir // testFixCwd is used to as a defer to testDir
func testFixCwd(t *testing.T, tmp, cwd string) { func testFixCwd(t *testing.T, tmp, cwd string) {
t.Helper()
if err := os.Chdir(cwd); err != nil { if err := os.Chdir(cwd); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -362,6 +394,8 @@ func testFixCwd(t *testing.T, tmp, cwd string) {
// The returned function should be deferred to properly clean up and restore // The returned function should be deferred to properly clean up and restore
// the original stdin. // the original stdin.
func testStdinPipe(t *testing.T, src io.Reader) func() { func testStdinPipe(t *testing.T, src io.Reader) func() {
t.Helper()
r, w, err := os.Pipe() r, w, err := os.Pipe()
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -390,6 +424,8 @@ func testStdinPipe(t *testing.T, src io.Reader) func() {
// not useful since the commands are configured to write to a cli.Ui, not // not useful since the commands are configured to write to a cli.Ui, not
// Stdout directly. Commands like `console` though use the raw stdout. // Stdout directly. Commands like `console` though use the raw stdout.
func testStdoutCapture(t *testing.T, dst io.Writer) func() { func testStdoutCapture(t *testing.T, dst io.Writer) func() {
t.Helper()
r, w, err := os.Pipe() r, w, err := os.Pipe()
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -424,6 +460,8 @@ func testStdoutCapture(t *testing.T, dst io.Writer) func() {
// in order to interactive prompts. The returned function must be called // in order to interactive prompts. The returned function must be called
// in a defer to clean up. // in a defer to clean up.
func testInteractiveInput(t *testing.T, answers []string) func() { func testInteractiveInput(t *testing.T, answers []string) func() {
t.Helper()
// Disable test mode so input is called // Disable test mode so input is called
test = false test = false
@ -443,6 +481,8 @@ func testInteractiveInput(t *testing.T, answers []string) func() {
// for calls to Input when the right question is asked. The key is the // for calls to Input when the right question is asked. The key is the
// question "Id" that is used. // question "Id" that is used.
func testInputMap(t *testing.T, answers map[string]string) func() { func testInputMap(t *testing.T, answers map[string]string) func() {
t.Helper()
// Disable test mode so input is called // Disable test mode so input is called
test = false test = false
@ -465,6 +505,8 @@ func testInputMap(t *testing.T, answers map[string]string) func() {
// backend. This returns the complete state that can be saved. Use // backend. This returns the complete state that can be saved. Use
// `testStateFileRemote` to write the returned state. // `testStateFileRemote` to write the returned state.
func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) { func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) {
t.Helper()
var b64md5 string var b64md5 string
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
@ -507,6 +549,8 @@ func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State
// testRemoteState is used to make a test HTTP server to return a given // testRemoteState is used to make a test HTTP server to return a given
// state file that can be used for testing legacy remote state. // state file that can be used for testing legacy remote state.
func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) { func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) {
t.Helper()
var b64md5 string var b64md5 string
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)

View File

@ -51,6 +51,8 @@ func canAccessNetwork() bool {
} }
func skipIfCannotAccessNetwork(t *testing.T) { func skipIfCannotAccessNetwork(t *testing.T) {
t.Helper()
if !canAccessNetwork() { if !canAccessNetwork() {
t.Skip("network access not allowed; use TF_ACC=1 to enable") t.Skip("network access not allowed; use TF_ACC=1 to enable")
} }

View File

@ -288,6 +288,11 @@ func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade
return err return err
} }
if err := terraform.CheckRequiredVersion(mod); err != nil {
c.Ui.Error(err.Error())
return err
}
var available discovery.PluginMetaSet var available discovery.PluginMetaSet
if upgrade { if upgrade {
// If we're in upgrade mode, we ignore any auto-installed plugins // If we're in upgrade mode, we ignore any auto-installed plugins

View File

@ -855,6 +855,27 @@ func TestInit_getProviderHaveLegacyVersion(t *testing.T) {
} }
} }
func TestInit_getProviderCheckRequiredVersion(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-check-required-version"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestInit_providerLockFile(t *testing.T) { func TestInit_providerLockFile(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) td := tempDir(t)

View File

@ -0,0 +1,3 @@
terraform {
required_version = "~> 0.9.0"
}

View File

@ -0,0 +1,4 @@
variable "var_without_default" {
type = "string"
}

View File

@ -0,0 +1 @@
var_without_default = "foo"

View File

@ -17,7 +17,7 @@ type ValidateCommand struct {
const defaultPath = "." const defaultPath = "."
func (c *ValidateCommand) Run(args []string) int { func (c *ValidateCommand) Run(args []string) int {
args, err := c.Meta.process(args, false) args, err := c.Meta.process(args, true)
if err != nil { if err != nil {
return 1 return 1
} }

View File

@ -1,9 +1,11 @@
package command package command
import ( import (
"os"
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -28,6 +30,28 @@ func TestValidateCommand(t *testing.T) {
} }
} }
func TestValidateCommandWithTfvarsFile(t *testing.T) {
// Create a temporary working directory that is empty because this test
// requires scanning the current working directory by validate command.
td := tempDir(t)
copy.CopyDir(testFixturePath("validate-valid/with-tfvars-file"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &ValidateCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestValidateFailingCommand(t *testing.T) { func TestValidateFailingCommand(t *testing.T) {
if ui, code := setupTest("validate-invalid"); code != 1 { if ui, code := setupTest("validate-invalid"); code != 1 {
t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())

View File

@ -143,6 +143,10 @@ func outputsStr(os []*Output) string {
result += fmt.Sprintf(" %s: %s\n", kind, str) result += fmt.Sprintf(" %s: %s\n", kind, str)
} }
} }
if o.Description != "" {
result += fmt.Sprintf(" description\n %s\n", o.Description)
}
} }
return strings.TrimSpace(result) return strings.TrimSpace(result)

View File

@ -499,6 +499,7 @@ func loadOutputsHcl(list *ast.ObjectList) ([]*Output, error) {
// Delete special keys // Delete special keys
delete(config, "depends_on") delete(config, "depends_on")
delete(config, "description")
rawConfig, err := NewRawConfig(config) rawConfig, err := NewRawConfig(config)
if err != nil { if err != nil {
@ -520,10 +521,23 @@ func loadOutputsHcl(list *ast.ObjectList) ([]*Output, error) {
} }
} }
// If we have a description field, then filter that
var description string
if o := listVal.Filter("description"); len(o.Items) > 0 {
err := hcl.DecodeObject(&description, o.Items[0].Val)
if err != nil {
return nil, fmt.Errorf(
"Error reading description for output %q: %s",
n,
err)
}
}
result = append(result, &Output{ result = append(result, &Output{
Name: n, Name: n,
RawConfig: rawConfig, RawConfig: rawConfig,
DependsOn: dependsOn, DependsOn: dependsOn,
Description: description,
}) })
} }

View File

@ -1058,6 +1058,11 @@ aws_instance.test (x1)
` `
const basicOutputsStr = ` const basicOutputsStr = `
web_id
vars
resource: aws_instance.web.id
description
The ID
web_ip web_ip
vars vars
resource: aws_instance.web.private_ip resource: aws_instance.web.private_ip

View File

@ -85,6 +85,11 @@ output "web_ip" {
value = "${aws_instance.web.private_ip}" value = "${aws_instance.web.private_ip}"
} }
output "web_id" {
description = "The ID"
value = "${aws_instance.web.id}"
}
atlas { atlas {
name = "mitchellh/foo" name = "mitchellh/foo"
} }

View File

@ -88,6 +88,10 @@
}, },
"output": { "output": {
"web_id": {
"description": "The ID",
"value": "${aws_instance.web.id}"
},
"web_ip": { "web_ip": {
"value": "${aws_instance.web.private_ip}" "value": "${aws_instance.web.private_ip}"
} }

View File

@ -6,6 +6,8 @@ import (
// TestRawConfig is used to create a RawConfig for testing. // TestRawConfig is used to create a RawConfig for testing.
func TestRawConfig(t *testing.T, c map[string]interface{}) *RawConfig { func TestRawConfig(t *testing.T, c map[string]interface{}) *RawConfig {
t.Helper()
cfg, err := NewRawConfig(c) cfg, err := NewRawConfig(c)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)

View File

@ -10,6 +10,8 @@ import (
// TestResourceDataRaw creates a ResourceData from a raw configuration map. // TestResourceDataRaw creates a ResourceData from a raw configuration map.
func TestResourceDataRaw( func TestResourceDataRaw(
t *testing.T, schema map[string]*Schema, raw map[string]interface{}) *ResourceData { t *testing.T, schema map[string]*Schema, raw map[string]interface{}) *ResourceData {
t.Helper()
c, err := config.NewRawConfig(raw) c, err := config.NewRawConfig(raw)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)

View File

@ -3,6 +3,7 @@ package validation
import ( import (
"fmt" "fmt"
"net" "net"
"regexp"
"strings" "strings"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -138,6 +139,8 @@ func CIDRNetwork(min, max int) schema.SchemaValidateFunc {
} }
} }
// ValidateJsonString is a SchemaValidateFunc which tests to make sure the
// supplied string is valid JSON.
func ValidateJsonString(v interface{}, k string) (ws []string, errors []error) { func ValidateJsonString(v interface{}, k string) (ws []string, errors []error) {
if _, err := structure.NormalizeJsonString(v); err != nil { if _, err := structure.NormalizeJsonString(v); err != nil {
errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err)) errors = append(errors, fmt.Errorf("%q contains an invalid JSON: %s", k, err))
@ -156,5 +159,13 @@ func ValidateListUniqueStrings(v interface{}, k string) (ws []string, errors []e
} }
} }
} }
}
// ValidateRegexp returns a SchemaValidateFunc which tests to make sure the
// supplied string is a valid regular expression.
func ValidateRegexp(v interface{}, k string) (ws []string, errors []error) {
if _, err := regexp.Compile(v.(string)); err != nil {
errors = append(errors, fmt.Errorf("%q: %s", k, err))
}
return return
} }

View File

@ -111,6 +111,20 @@ func TestValidationStringInSlice(t *testing.T) {
}) })
} }
func TestValidationRegexp(t *testing.T) {
runTestCases(t, []testCase{
{
val: ".*foo.*",
f: ValidateRegexp,
},
{
val: "foo(bar",
f: ValidateRegexp,
expectedErr: regexp.MustCompile(regexp.QuoteMeta("error parsing regexp: missing closing ): `foo(bar`")),
},
})
}
func TestValidateJsonString(t *testing.T) { func TestValidateJsonString(t *testing.T) {
type testCases struct { type testCases struct {
Value string Value string

View File

@ -11,6 +11,8 @@ import (
// that the given implementation is pre-loaded with the TestStateInitial // that the given implementation is pre-loaded with the TestStateInitial
// state. // state.
func TestState(t *testing.T, s State) { func TestState(t *testing.T, s State) {
t.Helper()
if err := s.RefreshState(); err != nil { if err := s.RefreshState(); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }

View File

@ -12,7 +12,6 @@ import (
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/experiment"
) )
// InputMode defines what sort of input will be asked for when Input // InputMode defines what sort of input will be asked for when Input
@ -123,7 +122,7 @@ type Context struct {
func NewContext(opts *ContextOpts) (*Context, error) { func NewContext(opts *ContextOpts) (*Context, error) {
// Validate the version requirement if it is given // Validate the version requirement if it is given
if opts.Module != nil { if opts.Module != nil {
if err := checkRequiredVersion(opts.Module); err != nil { if err := CheckRequiredVersion(opts.Module); err != nil {
return nil, err return nil, err
} }
} }
@ -465,7 +464,7 @@ func (c *Context) Input(mode InputMode) error {
} }
// Do the walk // Do the walk
if _, err := c.walk(graph, nil, walkInput); err != nil { if _, err := c.walk(graph, walkInput); err != nil {
return err return err
} }
} }
@ -506,7 +505,7 @@ func (c *Context) Apply() (*State, error) {
} }
// Walk the graph // Walk the graph
walker, err := c.walk(graph, graph, operation) walker, err := c.walk(graph, operation)
if len(walker.ValidationErrors) > 0 { if len(walker.ValidationErrors) > 0 {
err = multierror.Append(err, walker.ValidationErrors...) err = multierror.Append(err, walker.ValidationErrors...)
} }
@ -575,7 +574,7 @@ func (c *Context) Plan() (*Plan, error) {
} }
// Do the walk // Do the walk
walker, err := c.walk(graph, graph, operation) walker, err := c.walk(graph, operation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -630,7 +629,7 @@ func (c *Context) Refresh() (*State, error) {
} }
// Do the walk // Do the walk
if _, err := c.walk(graph, graph, walkRefresh); err != nil { if _, err := c.walk(graph, walkRefresh); err != nil {
return nil, err return nil, err
} }
@ -705,7 +704,7 @@ func (c *Context) Validate() ([]string, []error) {
} }
// Walk // Walk
walker, err := c.walk(graph, graph, walkValidate) walker, err := c.walk(graph, walkValidate)
if err != nil { if err != nil {
return nil, multierror.Append(errs, err).Errors return nil, multierror.Append(errs, err).Errors
} }
@ -792,33 +791,11 @@ func (c *Context) releaseRun() {
c.runContext = nil c.runContext = nil
} }
func (c *Context) walk( func (c *Context) walk(graph *Graph, operation walkOperation) (*ContextGraphWalker, error) {
graph, shadow *Graph, operation walkOperation) (*ContextGraphWalker, error) {
// Keep track of the "real" context which is the context that does // Keep track of the "real" context which is the context that does
// the real work: talking to real providers, modifying real state, etc. // the real work: talking to real providers, modifying real state, etc.
realCtx := c realCtx := c
// If we don't want shadowing, remove it
if !experiment.Enabled(experiment.X_shadow) {
shadow = nil
}
// Just log this so we can see it in a debug log
if !c.shadow {
log.Printf("[WARN] terraform: shadow graph disabled")
shadow = nil
}
// If we have a shadow graph, walk that as well
var shadowCtx *Context
var shadowCloser Shadow
if shadow != nil {
// Build the shadow context. In the process, override the real context
// with the one that is wrapped so that the shadow context can verify
// the results of the real.
realCtx, shadowCtx, shadowCloser = newShadowContext(c)
}
log.Printf("[DEBUG] Starting graph walk: %s", operation.String()) log.Printf("[DEBUG] Starting graph walk: %s", operation.String())
walker := &ContextGraphWalker{ walker := &ContextGraphWalker{
@ -837,90 +814,6 @@ func (c *Context) walk(
close(watchStop) close(watchStop)
<-watchWait <-watchWait
// If we have a shadow graph and we interrupted the real graph, then
// we just close the shadow and never verify it. It is non-trivial to
// recreate the exact execution state up until an interruption so this
// isn't supported with shadows at the moment.
if shadowCloser != nil && c.sh.Stopped() {
// Ignore the error result, there is nothing we could care about
shadowCloser.CloseShadow()
// Set it to nil so we don't do anything
shadowCloser = nil
}
// If we have a shadow graph, wait for that to complete.
if shadowCloser != nil {
// Build the graph walker for the shadow. We also wrap this in
// a panicwrap so that panics are captured. For the shadow graph,
// we just want panics to be normal errors rather than to crash
// Terraform.
shadowWalker := GraphWalkerPanicwrap(&ContextGraphWalker{
Context: shadowCtx,
Operation: operation,
})
// Kick off the shadow walk. This will block on any operations
// on the real walk so it is fine to start first.
log.Printf("[INFO] Starting shadow graph walk: %s", operation.String())
shadowCh := make(chan error)
go func() {
shadowCh <- shadow.Walk(shadowWalker)
}()
// Notify the shadow that we're done
if err := shadowCloser.CloseShadow(); err != nil {
c.shadowErr = multierror.Append(c.shadowErr, err)
}
// Wait for the walk to end
log.Printf("[DEBUG] Waiting for shadow graph to complete...")
shadowWalkErr := <-shadowCh
// Get any shadow errors
if err := shadowCloser.ShadowError(); err != nil {
c.shadowErr = multierror.Append(c.shadowErr, err)
}
// Verify the contexts (compare)
if err := shadowContextVerify(realCtx, shadowCtx); err != nil {
c.shadowErr = multierror.Append(c.shadowErr, err)
}
// At this point, if we're supposed to fail on error, then
// we PANIC. Some tests just verify that there is an error,
// so simply appending it to realErr and returning could hide
// shadow problems.
//
// This must be done BEFORE appending shadowWalkErr since the
// shadowWalkErr may include expected errors.
//
// We only do this if we don't have a real error. In the case of
// a real error, we can't guarantee what nodes were and weren't
// traversed in parallel scenarios so we can't guarantee no
// shadow errors.
if c.shadowErr != nil && contextFailOnShadowError && realErr == nil {
panic(multierror.Prefix(c.shadowErr, "shadow graph:"))
}
// Now, if we have a walk error, we append that through
if shadowWalkErr != nil {
c.shadowErr = multierror.Append(c.shadowErr, shadowWalkErr)
}
if c.shadowErr == nil {
log.Printf("[INFO] Shadow graph success!")
} else {
log.Printf("[ERROR] Shadow graph error: %s", c.shadowErr)
// If we're supposed to fail on shadow errors, then report it
if contextFailOnShadowError {
realErr = multierror.Append(realErr, multierror.Prefix(
c.shadowErr, "shadow graph:"))
}
}
}
return walker, realErr return walker, realErr
} }

View File

@ -66,7 +66,7 @@ func (c *Context) Import(opts *ImportOpts) (*State, error) {
} }
// Walk it // Walk it
if _, err := c.walk(graph, nil, walkImport); err != nil { if _, err := c.walk(graph, walkImport); err != nil {
return c.state, err return c.state, err
} }

View File

@ -1005,7 +1005,7 @@ func TestContext2Validate_PlanGraphBuilder(t *testing.T) {
t.Fatalf("error attmepting to Build PlanGraphBuilder: %s", err) t.Fatalf("error attmepting to Build PlanGraphBuilder: %s", err)
} }
defer c.acquireRun("validate-test")() defer c.acquireRun("validate-test")()
walker, err := c.walk(graph, graph, walkValidate) walker, err := c.walk(graph, walkValidate)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,28 +0,0 @@
package terraform
// Shadow is the interface that any "shadow" structures must implement.
//
// A shadow structure is an interface implementation (typically) that
// shadows a real implementation and verifies that the same behavior occurs
// on both. The semantics of this behavior are up to the interface itself.
//
// A shadow NEVER modifies real values or state. It must always be safe to use.
//
// For example, a ResourceProvider shadow ensures that the same operations
// are done on the same resources with the same configurations.
//
// The typical usage of a shadow following this interface is to complete
// the real operations, then call CloseShadow which tells the shadow that
// the real side is done. Then, once the shadow is also complete, call
// ShadowError to find any errors that may have been caught.
type Shadow interface {
// CloseShadow tells the shadow that the REAL implementation is
// complete. Therefore, any calls that would block should now return
// immediately since no more changes will happen to the real side.
CloseShadow() error
// ShadowError returns the errors that the shadow has found.
// This should be called AFTER CloseShadow and AFTER the shadow is
// known to be complete (no more calls to it).
ShadowError() error
}

View File

@ -1,273 +0,0 @@
package terraform
import (
"fmt"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/shadow"
)
// newShadowComponentFactory creates a shadowed contextComponentFactory
// so that requests to create new components result in both a real and
// shadow side.
func newShadowComponentFactory(
f contextComponentFactory) (contextComponentFactory, *shadowComponentFactory) {
// Create the shared data
shared := &shadowComponentFactoryShared{contextComponentFactory: f}
// Create the real side
real := &shadowComponentFactory{
shadowComponentFactoryShared: shared,
}
// Create the shadow
shadow := &shadowComponentFactory{
shadowComponentFactoryShared: shared,
Shadow: true,
}
return real, shadow
}
// shadowComponentFactory is the shadow side. Any components created
// with this factory are fake and will not cause real work to happen.
//
// Unlike other shadowers, the shadow component factory will allow the
// shadow to create _any_ component even if it is never requested on the
// real side. This is because errors will happen later downstream as function
// calls are made to the shadows that are never matched on the real side.
type shadowComponentFactory struct {
*shadowComponentFactoryShared
Shadow bool // True if this should return the shadow
lock sync.Mutex
}
func (f *shadowComponentFactory) ResourceProvider(
n, uid string) (ResourceProvider, error) {
f.lock.Lock()
defer f.lock.Unlock()
real, shadow, err := f.shadowComponentFactoryShared.ResourceProvider(n, uid)
var result ResourceProvider = real
if f.Shadow {
result = shadow
}
return result, err
}
func (f *shadowComponentFactory) ResourceProvisioner(
n, uid string) (ResourceProvisioner, error) {
f.lock.Lock()
defer f.lock.Unlock()
real, shadow, err := f.shadowComponentFactoryShared.ResourceProvisioner(n, uid)
var result ResourceProvisioner = real
if f.Shadow {
result = shadow
}
return result, err
}
// CloseShadow is called when the _real_ side is complete. This will cause
// all future blocking operations to return immediately on the shadow to
// ensure the shadow also completes.
func (f *shadowComponentFactory) CloseShadow() error {
// If we aren't the shadow, just return
if !f.Shadow {
return nil
}
// Lock ourselves so we don't modify state
f.lock.Lock()
defer f.lock.Unlock()
// Grab our shared state
shared := f.shadowComponentFactoryShared
// If we're already closed, its an error
if shared.closed {
return fmt.Errorf("component factory shadow already closed")
}
// Close all the providers and provisioners and return the error
var result error
for _, n := range shared.providerKeys {
_, shadow, err := shared.ResourceProvider(n, n)
if err == nil && shadow != nil {
if err := shadow.CloseShadow(); err != nil {
result = multierror.Append(result, err)
}
}
}
for _, n := range shared.provisionerKeys {
_, shadow, err := shared.ResourceProvisioner(n, n)
if err == nil && shadow != nil {
if err := shadow.CloseShadow(); err != nil {
result = multierror.Append(result, err)
}
}
}
// Mark ourselves as closed
shared.closed = true
return result
}
func (f *shadowComponentFactory) ShadowError() error {
// If we aren't the shadow, just return
if !f.Shadow {
return nil
}
// Lock ourselves so we don't modify state
f.lock.Lock()
defer f.lock.Unlock()
// Grab our shared state
shared := f.shadowComponentFactoryShared
// If we're not closed, its an error
if !shared.closed {
return fmt.Errorf("component factory must be closed to retrieve errors")
}
// Close all the providers and provisioners and return the error
var result error
for _, n := range shared.providerKeys {
_, shadow, err := shared.ResourceProvider(n, n)
if err == nil && shadow != nil {
if err := shadow.ShadowError(); err != nil {
result = multierror.Append(result, err)
}
}
}
for _, n := range shared.provisionerKeys {
_, shadow, err := shared.ResourceProvisioner(n, n)
if err == nil && shadow != nil {
if err := shadow.ShadowError(); err != nil {
result = multierror.Append(result, err)
}
}
}
return result
}
// shadowComponentFactoryShared is shared data between the two factories.
//
// It is NOT SAFE to run any function on this struct in parallel. Lock
// access to this struct.
type shadowComponentFactoryShared struct {
contextComponentFactory
closed bool
providers shadow.KeyedValue
providerKeys []string
provisioners shadow.KeyedValue
provisionerKeys []string
}
// shadowResourceProviderFactoryEntry is the entry that is stored in
// the Shadows key/value for a provider.
type shadowComponentFactoryProviderEntry struct {
Real ResourceProvider
Shadow shadowResourceProvider
Err error
}
type shadowComponentFactoryProvisionerEntry struct {
Real ResourceProvisioner
Shadow shadowResourceProvisioner
Err error
}
func (f *shadowComponentFactoryShared) ResourceProvider(
n, uid string) (ResourceProvider, shadowResourceProvider, error) {
// Determine if we already have a value
raw, ok := f.providers.ValueOk(uid)
if !ok {
// Build the entry
var entry shadowComponentFactoryProviderEntry
// No value, initialize. Create the original
p, err := f.contextComponentFactory.ResourceProvider(n, uid)
if err != nil {
entry.Err = err
p = nil // Just to be sure
}
if p != nil {
// Create the shadow
real, shadow := newShadowResourceProvider(p)
entry.Real = real
entry.Shadow = shadow
if f.closed {
shadow.CloseShadow()
}
}
// Store the value
f.providers.SetValue(uid, &entry)
f.providerKeys = append(f.providerKeys, uid)
raw = &entry
}
// Read the entry
entry, ok := raw.(*shadowComponentFactoryProviderEntry)
if !ok {
return nil, nil, fmt.Errorf("Unknown value for shadow provider: %#v", raw)
}
// Return
return entry.Real, entry.Shadow, entry.Err
}
func (f *shadowComponentFactoryShared) ResourceProvisioner(
n, uid string) (ResourceProvisioner, shadowResourceProvisioner, error) {
// Determine if we already have a value
raw, ok := f.provisioners.ValueOk(uid)
if !ok {
// Build the entry
var entry shadowComponentFactoryProvisionerEntry
// No value, initialize. Create the original
p, err := f.contextComponentFactory.ResourceProvisioner(n, uid)
if err != nil {
entry.Err = err
p = nil // Just to be sure
}
if p != nil {
// For now, just create a mock since we don't support provisioners yet
real, shadow := newShadowResourceProvisioner(p)
entry.Real = real
entry.Shadow = shadow
if f.closed {
shadow.CloseShadow()
}
}
// Store the value
f.provisioners.SetValue(uid, &entry)
f.provisionerKeys = append(f.provisionerKeys, uid)
raw = &entry
}
// Read the entry
entry, ok := raw.(*shadowComponentFactoryProvisionerEntry)
if !ok {
return nil, nil, fmt.Errorf("Unknown value for shadow provisioner: %#v", raw)
}
// Return
return entry.Real, entry.Shadow, entry.Err
}

View File

@ -1,158 +0,0 @@
package terraform
import (
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/copystructure"
)
// newShadowContext creates a new context that will shadow the given context
// when walking the graph. The resulting context should be used _only once_
// for a graph walk.
//
// The returned Shadow should be closed after the graph walk with the
// real context is complete. Errors from the shadow can be retrieved there.
//
// Most importantly, any operations done on the shadow context (the returned
// context) will NEVER affect the real context. All structures are deep
// copied, no real providers or resources are used, etc.
func newShadowContext(c *Context) (*Context, *Context, Shadow) {
// Copy the targets
targetRaw, err := copystructure.Copy(c.targets)
if err != nil {
panic(err)
}
// Copy the variables
varRaw, err := copystructure.Copy(c.variables)
if err != nil {
panic(err)
}
// Copy the provider inputs
providerInputRaw, err := copystructure.Copy(c.providerInputConfig)
if err != nil {
panic(err)
}
// The factories
componentsReal, componentsShadow := newShadowComponentFactory(c.components)
// Create the shadow
shadow := &Context{
components: componentsShadow,
destroy: c.destroy,
diff: c.diff.DeepCopy(),
hooks: nil,
meta: c.meta,
module: c.module,
state: c.state.DeepCopy(),
targets: targetRaw.([]string),
variables: varRaw.(map[string]interface{}),
// NOTE(mitchellh): This is not going to work for shadows that are
// testing that input results in the proper end state. At the time
// of writing, input is not used in any state-changing graph
// walks anyways, so this checks nothing. We set it to this to avoid
// any panics but even a "nil" value worked here.
uiInput: new(MockUIInput),
// Hardcoded to 4 since parallelism in the shadow doesn't matter
// a ton since we're doing far less compared to the real side
// and our operations are MUCH faster.
parallelSem: NewSemaphore(4),
providerInputConfig: providerInputRaw.(map[string]map[string]interface{}),
}
// Create the real context. This is effectively just a copy of
// the context given except we need to modify some of the values
// to point to the real side of a shadow so the shadow can compare values.
real := &Context{
// The fields below are changed.
components: componentsReal,
// The fields below are direct copies
destroy: c.destroy,
diff: c.diff,
// diffLock - no copy
hooks: c.hooks,
meta: c.meta,
module: c.module,
sh: c.sh,
state: c.state,
// stateLock - no copy
targets: c.targets,
uiInput: c.uiInput,
variables: c.variables,
// l - no copy
parallelSem: c.parallelSem,
providerInputConfig: c.providerInputConfig,
runContext: c.runContext,
runContextCancel: c.runContextCancel,
shadowErr: c.shadowErr,
}
return real, shadow, &shadowContextCloser{
Components: componentsShadow,
}
}
// shadowContextVerify takes the real and shadow context and verifies they
// have equal diffs and states.
func shadowContextVerify(real, shadow *Context) error {
var result error
// The states compared must be pruned so they're minimal/clean
real.state.prune()
shadow.state.prune()
// Compare the states
if !real.state.Equal(shadow.state) {
result = multierror.Append(result, fmt.Errorf(
"Real and shadow states do not match! "+
"Real state:\n\n%s\n\n"+
"Shadow state:\n\n%s\n\n",
real.state, shadow.state))
}
// Compare the diffs
if !real.diff.Equal(shadow.diff) {
result = multierror.Append(result, fmt.Errorf(
"Real and shadow diffs do not match! "+
"Real diff:\n\n%s\n\n"+
"Shadow diff:\n\n%s\n\n",
real.diff, shadow.diff))
}
return result
}
// shadowContextCloser is the io.Closer returned by newShadowContext that
// closes all the shadows and returns the results.
type shadowContextCloser struct {
Components *shadowComponentFactory
}
// Close closes the shadow context.
func (c *shadowContextCloser) CloseShadow() error {
return c.Components.CloseShadow()
}
func (c *shadowContextCloser) ShadowError() error {
err := c.Components.ShadowError()
if err == nil {
return nil
}
// This is a sad edge case: if the configuration contains uuid() at
// any point, we cannot reason aboyt the shadow execution. Tested
// with Context2Plan_shadowUuid.
if strings.Contains(err.Error(), "uuid()") {
err = nil
}
return err
}

View File

@ -1,815 +0,0 @@
package terraform
import (
"fmt"
"log"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/shadow"
)
// shadowResourceProvider implements ResourceProvider for the shadow
// eval context defined in eval_context_shadow.go.
//
// This is used to verify behavior with a real provider. This shouldn't
// be used directly.
type shadowResourceProvider interface {
ResourceProvider
Shadow
}
// newShadowResourceProvider creates a new shadowed ResourceProvider.
//
// This will assume a well behaved real ResourceProvider. For example,
// it assumes that the `Resources` call underneath doesn't change values
// since once it is called on the real provider, it will be cached and
// returned in the shadow since number of calls to that shouldn't affect
// actual behavior.
//
// However, with calls like Apply, call order is taken into account,
// parameters are checked for equality, etc.
func newShadowResourceProvider(p ResourceProvider) (ResourceProvider, shadowResourceProvider) {
// Create the shared data
shared := shadowResourceProviderShared{}
// Create the real provider that does actual work
real := &shadowResourceProviderReal{
ResourceProvider: p,
Shared: &shared,
}
// Create the shadow that watches the real value
shadow := &shadowResourceProviderShadow{
Shared: &shared,
resources: p.Resources(),
dataSources: p.DataSources(),
}
return real, shadow
}
// shadowResourceProviderReal is the real resource provider. Function calls
// to this will perform real work. This records the parameters and return
// values and call order for the shadow to reproduce.
type shadowResourceProviderReal struct {
ResourceProvider
Shared *shadowResourceProviderShared
}
func (p *shadowResourceProviderReal) Close() error {
var result error
if c, ok := p.ResourceProvider.(ResourceProviderCloser); ok {
result = c.Close()
}
p.Shared.CloseErr.SetValue(result)
return result
}
func (p *shadowResourceProviderReal) Input(
input UIInput, c *ResourceConfig) (*ResourceConfig, error) {
cCopy := c.DeepCopy()
result, err := p.ResourceProvider.Input(input, c)
p.Shared.Input.SetValue(&shadowResourceProviderInput{
Config: cCopy,
Result: result.DeepCopy(),
ResultErr: err,
})
return result, err
}
func (p *shadowResourceProviderReal) Validate(c *ResourceConfig) ([]string, []error) {
warns, errs := p.ResourceProvider.Validate(c)
p.Shared.Validate.SetValue(&shadowResourceProviderValidate{
Config: c.DeepCopy(),
ResultWarn: warns,
ResultErr: errs,
})
return warns, errs
}
func (p *shadowResourceProviderReal) Configure(c *ResourceConfig) error {
cCopy := c.DeepCopy()
err := p.ResourceProvider.Configure(c)
p.Shared.Configure.SetValue(&shadowResourceProviderConfigure{
Config: cCopy,
Result: err,
})
return err
}
func (p *shadowResourceProviderReal) Stop() error {
return p.ResourceProvider.Stop()
}
func (p *shadowResourceProviderReal) ValidateResource(
t string, c *ResourceConfig) ([]string, []error) {
key := t
configCopy := c.DeepCopy()
// Real operation
warns, errs := p.ResourceProvider.ValidateResource(t, c)
// Initialize to ensure we always have a wrapper with a lock
p.Shared.ValidateResource.Init(
key, &shadowResourceProviderValidateResourceWrapper{})
// Get the result
raw := p.Shared.ValidateResource.Value(key)
wrapper, ok := raw.(*shadowResourceProviderValidateResourceWrapper)
if !ok {
// If this fails then we just continue with our day... the shadow
// will fail to but there isn't much we can do.
log.Printf(
"[ERROR] unknown value in ValidateResource shadow value: %#v", raw)
return warns, errs
}
// Lock the wrapper for writing and record our call
wrapper.Lock()
defer wrapper.Unlock()
wrapper.Calls = append(wrapper.Calls, &shadowResourceProviderValidateResource{
Config: configCopy,
Warns: warns,
Errors: errs,
})
// With it locked, call SetValue again so that it triggers WaitForChange
p.Shared.ValidateResource.SetValue(key, wrapper)
// Return the result
return warns, errs
}
func (p *shadowResourceProviderReal) Apply(
info *InstanceInfo,
state *InstanceState,
diff *InstanceDiff) (*InstanceState, error) {
// Thse have to be copied before the call since call can modify
stateCopy := state.DeepCopy()
diffCopy := diff.DeepCopy()
result, err := p.ResourceProvider.Apply(info, state, diff)
p.Shared.Apply.SetValue(info.uniqueId(), &shadowResourceProviderApply{
State: stateCopy,
Diff: diffCopy,
Result: result.DeepCopy(),
ResultErr: err,
})
return result, err
}
func (p *shadowResourceProviderReal) Diff(
info *InstanceInfo,
state *InstanceState,
desired *ResourceConfig) (*InstanceDiff, error) {
// Thse have to be copied before the call since call can modify
stateCopy := state.DeepCopy()
desiredCopy := desired.DeepCopy()
result, err := p.ResourceProvider.Diff(info, state, desired)
p.Shared.Diff.SetValue(info.uniqueId(), &shadowResourceProviderDiff{
State: stateCopy,
Desired: desiredCopy,
Result: result.DeepCopy(),
ResultErr: err,
})
return result, err
}
func (p *shadowResourceProviderReal) Refresh(
info *InstanceInfo,
state *InstanceState) (*InstanceState, error) {
// Thse have to be copied before the call since call can modify
stateCopy := state.DeepCopy()
result, err := p.ResourceProvider.Refresh(info, state)
p.Shared.Refresh.SetValue(info.uniqueId(), &shadowResourceProviderRefresh{
State: stateCopy,
Result: result.DeepCopy(),
ResultErr: err,
})
return result, err
}
func (p *shadowResourceProviderReal) ValidateDataSource(
t string, c *ResourceConfig) ([]string, []error) {
key := t
configCopy := c.DeepCopy()
// Real operation
warns, errs := p.ResourceProvider.ValidateDataSource(t, c)
// Initialize
p.Shared.ValidateDataSource.Init(
key, &shadowResourceProviderValidateDataSourceWrapper{})
// Get the result
raw := p.Shared.ValidateDataSource.Value(key)
wrapper, ok := raw.(*shadowResourceProviderValidateDataSourceWrapper)
if !ok {
// If this fails then we just continue with our day... the shadow
// will fail to but there isn't much we can do.
log.Printf(
"[ERROR] unknown value in ValidateDataSource shadow value: %#v", raw)
return warns, errs
}
// Lock the wrapper for writing and record our call
wrapper.Lock()
defer wrapper.Unlock()
wrapper.Calls = append(wrapper.Calls, &shadowResourceProviderValidateDataSource{
Config: configCopy,
Warns: warns,
Errors: errs,
})
// Set it
p.Shared.ValidateDataSource.SetValue(key, wrapper)
// Return the result
return warns, errs
}
func (p *shadowResourceProviderReal) ReadDataDiff(
info *InstanceInfo,
desired *ResourceConfig) (*InstanceDiff, error) {
// These have to be copied before the call since call can modify
desiredCopy := desired.DeepCopy()
result, err := p.ResourceProvider.ReadDataDiff(info, desired)
p.Shared.ReadDataDiff.SetValue(info.uniqueId(), &shadowResourceProviderReadDataDiff{
Desired: desiredCopy,
Result: result.DeepCopy(),
ResultErr: err,
})
return result, err
}
func (p *shadowResourceProviderReal) ReadDataApply(
info *InstanceInfo,
diff *InstanceDiff) (*InstanceState, error) {
// Thse have to be copied before the call since call can modify
diffCopy := diff.DeepCopy()
result, err := p.ResourceProvider.ReadDataApply(info, diff)
p.Shared.ReadDataApply.SetValue(info.uniqueId(), &shadowResourceProviderReadDataApply{
Diff: diffCopy,
Result: result.DeepCopy(),
ResultErr: err,
})
return result, err
}
// shadowResourceProviderShadow is the shadow resource provider. Function
// calls never affect real resources. This is paired with the "real" side
// which must be called properly to enable recording.
type shadowResourceProviderShadow struct {
Shared *shadowResourceProviderShared
// Cached values that are expected to not change
resources []ResourceType
dataSources []DataSource
Error error // Error is the list of errors from the shadow
ErrorLock sync.Mutex
}
type shadowResourceProviderShared struct {
// NOTE: Anytime a value is added here, be sure to add it to
// the Close() method so that it is closed.
CloseErr shadow.Value
Input shadow.Value
Validate shadow.Value
Configure shadow.Value
ValidateResource shadow.KeyedValue
Apply shadow.KeyedValue
Diff shadow.KeyedValue
Refresh shadow.KeyedValue
ValidateDataSource shadow.KeyedValue
ReadDataDiff shadow.KeyedValue
ReadDataApply shadow.KeyedValue
}
func (p *shadowResourceProviderShared) Close() error {
return shadow.Close(p)
}
func (p *shadowResourceProviderShadow) CloseShadow() error {
err := p.Shared.Close()
if err != nil {
err = fmt.Errorf("close error: %s", err)
}
return err
}
func (p *shadowResourceProviderShadow) ShadowError() error {
return p.Error
}
func (p *shadowResourceProviderShadow) Resources() []ResourceType {
return p.resources
}
func (p *shadowResourceProviderShadow) DataSources() []DataSource {
return p.dataSources
}
func (p *shadowResourceProviderShadow) Close() error {
v := p.Shared.CloseErr.Value()
if v == nil {
return nil
}
return v.(error)
}
func (p *shadowResourceProviderShadow) Input(
input UIInput, c *ResourceConfig) (*ResourceConfig, error) {
// Get the result of the input call
raw := p.Shared.Input.Value()
if raw == nil {
return nil, nil
}
result, ok := raw.(*shadowResourceProviderInput)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'input' shadow value: %#v", raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !c.Equal(result.Config) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Input had unequal configurations (real, then shadow):\n\n%#v\n\n%#v",
result.Config, c))
p.ErrorLock.Unlock()
}
// Return the results
return result.Result, result.ResultErr
}
func (p *shadowResourceProviderShadow) Validate(c *ResourceConfig) ([]string, []error) {
// Get the result of the validate call
raw := p.Shared.Validate.Value()
if raw == nil {
return nil, nil
}
result, ok := raw.(*shadowResourceProviderValidate)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'validate' shadow value: %#v", raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !c.Equal(result.Config) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Validate had unequal configurations (real, then shadow):\n\n%#v\n\n%#v",
result.Config, c))
p.ErrorLock.Unlock()
}
// Return the results
return result.ResultWarn, result.ResultErr
}
func (p *shadowResourceProviderShadow) Configure(c *ResourceConfig) error {
// Get the result of the call
raw := p.Shared.Configure.Value()
if raw == nil {
return nil
}
result, ok := raw.(*shadowResourceProviderConfigure)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'configure' shadow value: %#v", raw))
return nil
}
// Compare the parameters, which should be identical
if !c.Equal(result.Config) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Configure had unequal configurations (real, then shadow):\n\n%#v\n\n%#v",
result.Config, c))
p.ErrorLock.Unlock()
}
// Return the results
return result.Result
}
// Stop returns immediately.
func (p *shadowResourceProviderShadow) Stop() error {
return nil
}
func (p *shadowResourceProviderShadow) ValidateResource(t string, c *ResourceConfig) ([]string, []error) {
// Unique key
key := t
// Get the initial value
raw := p.Shared.ValidateResource.Value(key)
// Find a validation with our configuration
var result *shadowResourceProviderValidateResource
for {
// Get the value
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateResource' call for %q:\n\n%#v",
key, c))
return nil, nil
}
wrapper, ok := raw.(*shadowResourceProviderValidateResourceWrapper)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateResource' shadow value for %q: %#v", key, raw))
return nil, nil
}
// Look for the matching call with our configuration
wrapper.RLock()
for _, call := range wrapper.Calls {
if call.Config.Equal(c) {
result = call
break
}
}
wrapper.RUnlock()
// If we found a result, exit
if result != nil {
break
}
// Wait for a change so we can get the wrapper again
raw = p.Shared.ValidateResource.WaitForChange(key)
}
return result.Warns, result.Errors
}
func (p *shadowResourceProviderShadow) Apply(
info *InstanceInfo,
state *InstanceState,
diff *InstanceDiff) (*InstanceState, error) {
// Unique key
key := info.uniqueId()
raw := p.Shared.Apply.Value(key)
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'apply' call for %q:\n\n%#v\n\n%#v",
key, state, diff))
return nil, nil
}
result, ok := raw.(*shadowResourceProviderApply)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'apply' shadow value for %q: %#v", key, raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !state.Equal(result.State) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Apply %q: state had unequal states (real, then shadow):\n\n%#v\n\n%#v",
key, result.State, state))
p.ErrorLock.Unlock()
}
if !diff.Equal(result.Diff) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Apply %q: unequal diffs (real, then shadow):\n\n%#v\n\n%#v",
key, result.Diff, diff))
p.ErrorLock.Unlock()
}
return result.Result, result.ResultErr
}
func (p *shadowResourceProviderShadow) Diff(
info *InstanceInfo,
state *InstanceState,
desired *ResourceConfig) (*InstanceDiff, error) {
// Unique key
key := info.uniqueId()
raw := p.Shared.Diff.Value(key)
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'diff' call for %q:\n\n%#v\n\n%#v",
key, state, desired))
return nil, nil
}
result, ok := raw.(*shadowResourceProviderDiff)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'diff' shadow value for %q: %#v", key, raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !state.Equal(result.State) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Diff %q had unequal states (real, then shadow):\n\n%#v\n\n%#v",
key, result.State, state))
p.ErrorLock.Unlock()
}
if !desired.Equal(result.Desired) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Diff %q had unequal states (real, then shadow):\n\n%#v\n\n%#v",
key, result.Desired, desired))
p.ErrorLock.Unlock()
}
return result.Result, result.ResultErr
}
func (p *shadowResourceProviderShadow) Refresh(
info *InstanceInfo,
state *InstanceState) (*InstanceState, error) {
// Unique key
key := info.uniqueId()
raw := p.Shared.Refresh.Value(key)
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'refresh' call for %q:\n\n%#v",
key, state))
return nil, nil
}
result, ok := raw.(*shadowResourceProviderRefresh)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'refresh' shadow value: %#v", raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !state.Equal(result.State) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Refresh %q had unequal states (real, then shadow):\n\n%#v\n\n%#v",
key, result.State, state))
p.ErrorLock.Unlock()
}
return result.Result, result.ResultErr
}
func (p *shadowResourceProviderShadow) ValidateDataSource(
t string, c *ResourceConfig) ([]string, []error) {
// Unique key
key := t
// Get the initial value
raw := p.Shared.ValidateDataSource.Value(key)
// Find a validation with our configuration
var result *shadowResourceProviderValidateDataSource
for {
// Get the value
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateDataSource' call for %q:\n\n%#v",
key, c))
return nil, nil
}
wrapper, ok := raw.(*shadowResourceProviderValidateDataSourceWrapper)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateDataSource' shadow value: %#v", raw))
return nil, nil
}
// Look for the matching call with our configuration
wrapper.RLock()
for _, call := range wrapper.Calls {
if call.Config.Equal(c) {
result = call
break
}
}
wrapper.RUnlock()
// If we found a result, exit
if result != nil {
break
}
// Wait for a change so we can get the wrapper again
raw = p.Shared.ValidateDataSource.WaitForChange(key)
}
return result.Warns, result.Errors
}
func (p *shadowResourceProviderShadow) ReadDataDiff(
info *InstanceInfo,
desired *ResourceConfig) (*InstanceDiff, error) {
// Unique key
key := info.uniqueId()
raw := p.Shared.ReadDataDiff.Value(key)
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ReadDataDiff' call for %q:\n\n%#v",
key, desired))
return nil, nil
}
result, ok := raw.(*shadowResourceProviderReadDataDiff)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ReadDataDiff' shadow value for %q: %#v", key, raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !desired.Equal(result.Desired) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"ReadDataDiff %q had unequal configs (real, then shadow):\n\n%#v\n\n%#v",
key, result.Desired, desired))
p.ErrorLock.Unlock()
}
return result.Result, result.ResultErr
}
func (p *shadowResourceProviderShadow) ReadDataApply(
info *InstanceInfo,
d *InstanceDiff) (*InstanceState, error) {
// Unique key
key := info.uniqueId()
raw := p.Shared.ReadDataApply.Value(key)
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ReadDataApply' call for %q:\n\n%#v",
key, d))
return nil, nil
}
result, ok := raw.(*shadowResourceProviderReadDataApply)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ReadDataApply' shadow value for %q: %#v", key, raw))
return nil, nil
}
// Compare the parameters, which should be identical
if !d.Equal(result.Diff) {
p.ErrorLock.Lock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"ReadDataApply: unequal diffs (real, then shadow):\n\n%#v\n\n%#v",
result.Diff, d))
p.ErrorLock.Unlock()
}
return result.Result, result.ResultErr
}
func (p *shadowResourceProviderShadow) ImportState(info *InstanceInfo, id string) ([]*InstanceState, error) {
panic("import not supported by shadow graph")
}
// The structs for the various function calls are put below. These structs
// are used to carry call information across the real/shadow boundaries.
type shadowResourceProviderInput struct {
Config *ResourceConfig
Result *ResourceConfig
ResultErr error
}
type shadowResourceProviderValidate struct {
Config *ResourceConfig
ResultWarn []string
ResultErr []error
}
type shadowResourceProviderConfigure struct {
Config *ResourceConfig
Result error
}
type shadowResourceProviderValidateResourceWrapper struct {
sync.RWMutex
Calls []*shadowResourceProviderValidateResource
}
type shadowResourceProviderValidateResource struct {
Config *ResourceConfig
Warns []string
Errors []error
}
type shadowResourceProviderApply struct {
State *InstanceState
Diff *InstanceDiff
Result *InstanceState
ResultErr error
}
type shadowResourceProviderDiff struct {
State *InstanceState
Desired *ResourceConfig
Result *InstanceDiff
ResultErr error
}
type shadowResourceProviderRefresh struct {
State *InstanceState
Result *InstanceState
ResultErr error
}
type shadowResourceProviderValidateDataSourceWrapper struct {
sync.RWMutex
Calls []*shadowResourceProviderValidateDataSource
}
type shadowResourceProviderValidateDataSource struct {
Config *ResourceConfig
Warns []string
Errors []error
}
type shadowResourceProviderReadDataDiff struct {
Desired *ResourceConfig
Result *InstanceDiff
ResultErr error
}
type shadowResourceProviderReadDataApply struct {
Diff *InstanceDiff
Result *InstanceState
ResultErr error
}

View File

@ -1,531 +0,0 @@
package terraform
import (
"fmt"
"reflect"
"testing"
"time"
)
func TestShadowResourceProvider_impl(t *testing.T) {
var _ Shadow = new(shadowResourceProviderShadow)
}
func TestShadowResourceProvider_cachedValues(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Resources
{
actual := shadow.Resources()
expected := real.Resources()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad:\n\n%#v\n\n%#v", actual, expected)
}
}
// DataSources
{
actual := shadow.DataSources()
expected := real.DataSources()
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad:\n\n%#v\n\n%#v", actual, expected)
}
}
}
func TestShadowResourceProviderInput(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
ui := new(MockUIInput)
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnConfig := testResourceConfig(t, map[string]interface{}{
"bar": "baz",
})
// Configure the mock
mock.InputReturnConfig = returnConfig
// Verify that it blocks until the real input is called
var actual *ResourceConfig
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
actual, err = shadow.Input(ui, config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real input
realResult, realErr := real.Input(ui, config)
if !realResult.Equal(returnConfig) {
t.Fatalf("bad: %#v", realResult)
}
if realErr != nil {
t.Fatalf("bad: %s", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !actual.Equal(returnConfig) {
t.Fatalf("bad: %#v", actual)
}
if err != nil {
t.Fatalf("bad: %s", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderInput_badInput(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
ui := new(MockUIInput)
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
configBad := testResourceConfig(t, map[string]interface{}{
"foo": "nope",
})
// Call the real with one
real.Input(ui, config)
// Call the shadow with another
_, err := shadow.Input(ui, configBad)
if err != nil {
t.Fatalf("bad: %s", err)
}
// Verify we have an error
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err == nil {
t.Fatal("should error")
}
}
func TestShadowResourceProviderValidate(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnWarns := []string{"foo"}
returnErrs := []error{fmt.Errorf("bar")}
// Configure the mock
mock.ValidateReturnWarns = returnWarns
mock.ValidateReturnErrors = returnErrs
// Verify that it blocks until the real func is called
var warns []string
var errs []error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
warns, errs = shadow.Validate(config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realWarns, realErrs := real.Validate(config)
if !reflect.DeepEqual(realWarns, returnWarns) {
t.Fatalf("bad: %#v", realWarns)
}
if !reflect.DeepEqual(realErrs, returnErrs) {
t.Fatalf("bad: %#v", realWarns)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(warns, returnWarns) {
t.Fatalf("bad: %#v", warns)
}
if !reflect.DeepEqual(errs, returnErrs) {
t.Fatalf("bad: %#v", errs)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderValidate_badInput(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
configBad := testResourceConfig(t, map[string]interface{}{
"foo": "nope",
})
// Call the real with one
real.Validate(config)
// Call the shadow with another
shadow.Validate(configBad)
// Verify we have an error
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err == nil {
t.Fatal("should error")
}
}
func TestShadowResourceProviderConfigure(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnErr := fmt.Errorf("bar")
// Configure the mock
mock.ConfigureReturnError = returnErr
// Verify that it blocks until the real func is called
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
err = shadow.Configure(config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realErr := real.Configure(config)
if !reflect.DeepEqual(realErr, returnErr) {
t.Fatalf("bad: %#v", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(err, returnErr) {
t.Fatalf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderConfigure_badInput(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
configBad := testResourceConfig(t, map[string]interface{}{
"foo": "nope",
})
// Call the real with one
real.Configure(config)
// Call the shadow with another
shadow.Configure(configBad)
// Verify we have an error
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err == nil {
t.Fatal("should error")
}
}
func TestShadowResourceProviderApply(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
info := &InstanceInfo{Id: "foo"}
state := &InstanceState{ID: "foo"}
diff := &InstanceDiff{Destroy: true}
mockResult := &InstanceState{ID: "bar"}
// Configure the mock
mock.ApplyReturn = mockResult
// Verify that it blocks until the real func is called
var result *InstanceState
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
result, err = shadow.Apply(info, state, diff)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realResult, realErr := real.Apply(info, state, diff)
if !realResult.Equal(mockResult) {
t.Fatalf("bad: %#v", realResult)
}
if realErr != nil {
t.Fatalf("bad: %#v", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !result.Equal(mockResult) {
t.Fatalf("bad: %#v", result)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderApply_modifyDiff(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
info := &InstanceInfo{Id: "foo"}
state := &InstanceState{ID: "foo"}
diff := &InstanceDiff{}
mockResult := &InstanceState{ID: "foo"}
// Configure the mock
mock.ApplyFn = func(
info *InstanceInfo,
s *InstanceState, d *InstanceDiff) (*InstanceState, error) {
d.Destroy = true
return s, nil
}
// Call the real func
realResult, realErr := real.Apply(info, state.DeepCopy(), diff.DeepCopy())
if !realResult.Equal(mockResult) {
t.Fatalf("bad: %#v", realResult)
}
if realErr != nil {
t.Fatalf("bad: %#v", realErr)
}
// Verify the shadow returned the same values
result, err := shadow.Apply(info, state.DeepCopy(), diff.DeepCopy())
if !result.Equal(mockResult) {
t.Fatalf("bad: %#v", result)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderApply_modifyState(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
info := &InstanceInfo{Id: "foo"}
state := &InstanceState{ID: ""}
diff := &InstanceDiff{}
mockResult := &InstanceState{ID: "foo"}
// Configure the mock
mock.ApplyFn = func(
info *InstanceInfo,
s *InstanceState, d *InstanceDiff) (*InstanceState, error) {
s.ID = "foo"
return s, nil
}
// Call the real func
realResult, realErr := real.Apply(info, state.DeepCopy(), diff)
if !realResult.Equal(mockResult) {
t.Fatalf("bad: %#v", realResult)
}
if realErr != nil {
t.Fatalf("bad: %#v", realErr)
}
// Verify the shadow returned the same values
result, err := shadow.Apply(info, state.DeepCopy(), diff)
if !result.Equal(mockResult) {
t.Fatalf("bad: %#v", result)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderDiff(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
info := &InstanceInfo{Id: "foo"}
state := &InstanceState{ID: "foo"}
desired := testResourceConfig(t, map[string]interface{}{"foo": "bar"})
mockResult := &InstanceDiff{Destroy: true}
// Configure the mock
mock.DiffReturn = mockResult
// Verify that it blocks until the real func is called
var result *InstanceDiff
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
result, err = shadow.Diff(info, state, desired)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realResult, realErr := real.Diff(info, state, desired)
if !reflect.DeepEqual(realResult, mockResult) {
t.Fatalf("bad: %#v", realResult)
}
if realErr != nil {
t.Fatalf("bad: %#v", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(result, mockResult) {
t.Fatalf("bad: %#v", result)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProviderRefresh(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)
// Test values
info := &InstanceInfo{Id: "foo"}
state := &InstanceState{ID: "foo"}
mockResult := &InstanceState{ID: "bar"}
// Configure the mock
mock.RefreshReturn = mockResult
// Verify that it blocks until the real func is called
var result *InstanceState
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
result, err = shadow.Refresh(info, state)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realResult, realErr := real.Refresh(info, state)
if !realResult.Equal(mockResult) {
t.Fatalf("bad: %#v", realResult)
}
if realErr != nil {
t.Fatalf("bad: %#v", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !result.Equal(mockResult) {
t.Fatalf("bad: %#v", result)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}

View File

@ -1,282 +0,0 @@
package terraform
import (
"fmt"
"io"
"log"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/shadow"
)
// shadowResourceProvisioner implements ResourceProvisioner for the shadow
// eval context defined in eval_context_shadow.go.
//
// This is used to verify behavior with a real provisioner. This shouldn't
// be used directly.
type shadowResourceProvisioner interface {
ResourceProvisioner
Shadow
}
// newShadowResourceProvisioner creates a new shadowed ResourceProvisioner.
func newShadowResourceProvisioner(
p ResourceProvisioner) (ResourceProvisioner, shadowResourceProvisioner) {
// Create the shared data
shared := shadowResourceProvisionerShared{
Validate: shadow.ComparedValue{
Func: shadowResourceProvisionerValidateCompare,
},
}
// Create the real provisioner that does actual work
real := &shadowResourceProvisionerReal{
ResourceProvisioner: p,
Shared: &shared,
}
// Create the shadow that watches the real value
shadow := &shadowResourceProvisionerShadow{
Shared: &shared,
}
return real, shadow
}
// shadowResourceProvisionerReal is the real resource provisioner. Function calls
// to this will perform real work. This records the parameters and return
// values and call order for the shadow to reproduce.
type shadowResourceProvisionerReal struct {
ResourceProvisioner
Shared *shadowResourceProvisionerShared
}
func (p *shadowResourceProvisionerReal) Close() error {
var result error
if c, ok := p.ResourceProvisioner.(ResourceProvisionerCloser); ok {
result = c.Close()
}
p.Shared.CloseErr.SetValue(result)
return result
}
func (p *shadowResourceProvisionerReal) Validate(c *ResourceConfig) ([]string, []error) {
warns, errs := p.ResourceProvisioner.Validate(c)
p.Shared.Validate.SetValue(&shadowResourceProvisionerValidate{
Config: c,
ResultWarn: warns,
ResultErr: errs,
})
return warns, errs
}
func (p *shadowResourceProvisionerReal) Apply(
output UIOutput, s *InstanceState, c *ResourceConfig) error {
err := p.ResourceProvisioner.Apply(output, s, c)
// Write the result, grab a lock for writing. This should nver
// block long since the operations below don't block.
p.Shared.ApplyLock.Lock()
defer p.Shared.ApplyLock.Unlock()
key := s.ID
raw, ok := p.Shared.Apply.ValueOk(key)
if !ok {
// Setup a new value
raw = &shadow.ComparedValue{
Func: shadowResourceProvisionerApplyCompare,
}
// Set it
p.Shared.Apply.SetValue(key, raw)
}
compareVal, ok := raw.(*shadow.ComparedValue)
if !ok {
// Just log and return so that we don't cause the real side
// any side effects.
log.Printf("[ERROR] unknown value in 'apply': %#v", raw)
return err
}
// Write the resulting value
compareVal.SetValue(&shadowResourceProvisionerApply{
Config: c,
ResultErr: err,
})
return err
}
func (p *shadowResourceProvisionerReal) Stop() error {
return p.ResourceProvisioner.Stop()
}
// shadowResourceProvisionerShadow is the shadow resource provisioner. Function
// calls never affect real resources. This is paired with the "real" side
// which must be called properly to enable recording.
type shadowResourceProvisionerShadow struct {
Shared *shadowResourceProvisionerShared
Error error // Error is the list of errors from the shadow
ErrorLock sync.Mutex
}
type shadowResourceProvisionerShared struct {
// NOTE: Anytime a value is added here, be sure to add it to
// the Close() method so that it is closed.
CloseErr shadow.Value
Validate shadow.ComparedValue
Apply shadow.KeyedValue
ApplyLock sync.Mutex // For writing only
}
func (p *shadowResourceProvisionerShared) Close() error {
closers := []io.Closer{
&p.CloseErr,
}
for _, c := range closers {
// This should never happen, but we don't panic because a panic
// could affect the real behavior of Terraform and a shadow should
// never be able to do that.
if err := c.Close(); err != nil {
return err
}
}
return nil
}
func (p *shadowResourceProvisionerShadow) CloseShadow() error {
err := p.Shared.Close()
if err != nil {
err = fmt.Errorf("close error: %s", err)
}
return err
}
func (p *shadowResourceProvisionerShadow) ShadowError() error {
return p.Error
}
func (p *shadowResourceProvisionerShadow) Close() error {
v := p.Shared.CloseErr.Value()
if v == nil {
return nil
}
return v.(error)
}
func (p *shadowResourceProvisionerShadow) Validate(c *ResourceConfig) ([]string, []error) {
// Get the result of the validate call
raw := p.Shared.Validate.Value(c)
if raw == nil {
return nil, nil
}
result, ok := raw.(*shadowResourceProvisionerValidate)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'validate' shadow value: %#v", raw))
return nil, nil
}
// We don't need to compare configurations because we key on the
// configuration so just return right away.
return result.ResultWarn, result.ResultErr
}
func (p *shadowResourceProvisionerShadow) Apply(
output UIOutput, s *InstanceState, c *ResourceConfig) error {
// Get the value based on the key
key := s.ID
raw := p.Shared.Apply.Value(key)
if raw == nil {
return nil
}
compareVal, ok := raw.(*shadow.ComparedValue)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'apply' shadow value: %#v", raw))
return nil
}
// With the compared value, we compare against our config
raw = compareVal.Value(c)
if raw == nil {
return nil
}
result, ok := raw.(*shadowResourceProvisionerApply)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'apply' shadow value: %#v", raw))
return nil
}
return result.ResultErr
}
func (p *shadowResourceProvisionerShadow) Stop() error {
// For the shadow, we always just return nil since a Stop indicates
// that we were interrupted and shadows are disabled during interrupts
// anyways.
return nil
}
// The structs for the various function calls are put below. These structs
// are used to carry call information across the real/shadow boundaries.
type shadowResourceProvisionerValidate struct {
Config *ResourceConfig
ResultWarn []string
ResultErr []error
}
type shadowResourceProvisionerApply struct {
Config *ResourceConfig
ResultErr error
}
func shadowResourceProvisionerValidateCompare(k, v interface{}) bool {
c, ok := k.(*ResourceConfig)
if !ok {
return false
}
result, ok := v.(*shadowResourceProvisionerValidate)
if !ok {
return false
}
return c.Equal(result.Config)
}
func shadowResourceProvisionerApplyCompare(k, v interface{}) bool {
c, ok := k.(*ResourceConfig)
if !ok {
return false
}
result, ok := v.(*shadowResourceProvisionerApply)
if !ok {
return false
}
return c.Equal(result.Config)
}

View File

@ -1,178 +0,0 @@
package terraform
import (
"errors"
"fmt"
"reflect"
"testing"
"time"
)
func TestShadowResourceProvisioner_impl(t *testing.T) {
var _ Shadow = new(shadowResourceProvisionerShadow)
}
func TestShadowResourceProvisionerValidate(t *testing.T) {
mock := new(MockResourceProvisioner)
real, shadow := newShadowResourceProvisioner(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnWarns := []string{"foo"}
returnErrs := []error{fmt.Errorf("bar")}
// Configure the mock
mock.ValidateReturnWarns = returnWarns
mock.ValidateReturnErrors = returnErrs
// Verify that it blocks until the real func is called
var warns []string
var errs []error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
warns, errs = shadow.Validate(config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realWarns, realErrs := real.Validate(config)
if !reflect.DeepEqual(realWarns, returnWarns) {
t.Fatalf("bad: %#v", realWarns)
}
if !reflect.DeepEqual(realErrs, returnErrs) {
t.Fatalf("bad: %#v", realWarns)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(warns, returnWarns) {
t.Fatalf("bad: %#v", warns)
}
if !reflect.DeepEqual(errs, returnErrs) {
t.Fatalf("bad: %#v", errs)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProvisionerValidate_diff(t *testing.T) {
mock := new(MockResourceProvisioner)
real, shadow := newShadowResourceProvisioner(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnWarns := []string{"foo"}
returnErrs := []error{fmt.Errorf("bar")}
// Configure the mock
mock.ValidateReturnWarns = returnWarns
mock.ValidateReturnErrors = returnErrs
// Run a real validation with a config
real.Validate(testResourceConfig(t, map[string]interface{}{"bar": "baz"}))
// Verify that it blocks until the real func is called
var warns []string
var errs []error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
warns, errs = shadow.Validate(config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realWarns, realErrs := real.Validate(config)
if !reflect.DeepEqual(realWarns, returnWarns) {
t.Fatalf("bad: %#v", realWarns)
}
if !reflect.DeepEqual(realErrs, returnErrs) {
t.Fatalf("bad: %#v", realWarns)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(warns, returnWarns) {
t.Fatalf("bad: %#v", warns)
}
if !reflect.DeepEqual(errs, returnErrs) {
t.Fatalf("bad: %#v", errs)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProvisionerApply(t *testing.T) {
mock := new(MockResourceProvisioner)
real, shadow := newShadowResourceProvisioner(mock)
// Test values
output := new(MockUIOutput)
state := &InstanceState{ID: "foo"}
config := testResourceConfig(t, map[string]interface{}{"foo": "bar"})
mockReturn := errors.New("err")
// Configure the mock
mock.ApplyReturnError = mockReturn
// Verify that it blocks until the real func is called
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
err = shadow.Apply(output, state, config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realErr := real.Apply(output, state, config)
if realErr != mockReturn {
t.Fatalf("bad: %#v", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if err != mockReturn {
t.Errorf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err != nil {
t.Fatalf("bad: %s", err)
}
}

View File

@ -48,6 +48,8 @@ func TestMain(m *testing.M) {
} }
func tempDir(t *testing.T) string { func tempDir(t *testing.T) string {
t.Helper()
dir, err := ioutil.TempDir("", "tf") dir, err := ioutil.TempDir("", "tf")
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -63,6 +65,8 @@ func tempDir(t *testing.T) string {
// a function to defer to reset the old value. // a function to defer to reset the old value.
// the old value that should be set via a defer. // the old value that should be set via a defer.
func tempEnv(t *testing.T, k string, v string) func() { func tempEnv(t *testing.T, k string, v string) func() {
t.Helper()
old, oldOk := os.LookupEnv(k) old, oldOk := os.LookupEnv(k)
os.Setenv(k, v) os.Setenv(k, v)
return func() { return func() {
@ -75,6 +79,8 @@ func tempEnv(t *testing.T, k string, v string) func() {
} }
func testConfig(t *testing.T, name string) *config.Config { func testConfig(t *testing.T, name string) *config.Config {
t.Helper()
c, err := config.LoadFile(filepath.Join(fixtureDir, name, "main.tf")) c, err := config.LoadFile(filepath.Join(fixtureDir, name, "main.tf"))
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -84,6 +90,8 @@ func testConfig(t *testing.T, name string) *config.Config {
} }
func testModule(t *testing.T, name string) *module.Tree { func testModule(t *testing.T, name string) *module.Tree {
t.Helper()
mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name)) mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name))
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -100,6 +108,8 @@ func testModule(t *testing.T, name string) *module.Tree {
// testModuleInline takes a map of path -> config strings and yields a config // testModuleInline takes a map of path -> config strings and yields a config
// structure with those files loaded from disk // structure with those files loaded from disk
func testModuleInline(t *testing.T, config map[string]string) *module.Tree { func testModuleInline(t *testing.T, config map[string]string) *module.Tree {
t.Helper()
cfgPath, err := ioutil.TempDir("", "tf-test") cfgPath, err := ioutil.TempDir("", "tf-test")
if err != nil { if err != nil {
t.Errorf("Error creating temporary directory for config: %s", err) t.Errorf("Error creating temporary directory for config: %s", err)
@ -146,6 +156,8 @@ func testModuleInline(t *testing.T, config map[string]string) *module.Tree {
} }
func testStringMatch(t *testing.T, s fmt.Stringer, expected string) { func testStringMatch(t *testing.T, s fmt.Stringer, expected string) {
t.Helper()
actual := strings.TrimSpace(s.String()) actual := strings.TrimSpace(s.String())
expected = strings.TrimSpace(expected) expected = strings.TrimSpace(expected)
if actual != expected { if actual != expected {

View File

@ -8,17 +8,17 @@ import (
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
) )
// checkRequiredVersion verifies that any version requirements specified by // CheckRequiredVersion verifies that any version requirements specified by
// the configuration are met. // the configuration are met.
// //
// This checks the root module as well as any additional version requirements // This checks the root module as well as any additional version requirements
// from child modules. // from child modules.
// //
// This is tested in context_test.go. // This is tested in context_test.go.
func checkRequiredVersion(m *module.Tree) error { func CheckRequiredVersion(m *module.Tree) error {
// Check any children // Check any children
for _, c := range m.Children() { for _, c := range m.Children() {
if err := checkRequiredVersion(c); err != nil { if err := CheckRequiredVersion(c); err != nil {
return err return err
} }
} }

View File

@ -110,6 +110,12 @@ This special argument applies to _all_ providers.
view the specified version constraints for all providers used in the view the specified version constraints for all providers used in the
current configuration. current configuration.
When `terraform init` is re-run with providers already installed, it will
use an already-installed provider that meets the constraints in preference
to downloading a new version. To upgrade to the latest acceptable version
of each provider, run `terraform init -upgrade`. This command also upgrades
to the latest versions of all Terraform modules.
## Multiple Provider Instances ## Multiple Provider Instances
You can define multiple instances of the same provider in order to support You can define multiple instances of the same provider in order to support

View File

@ -84,7 +84,7 @@ Additionally, because these map directly to variables, module configuration can
## Outputs ## Outputs
Modules can also specify their own [outputs](/docs/configuration/outputs.html). These outputs can be referenced in other places in your configuration, for example: Modules encapsulate their resources. A resource in one module cannot directly depend on resources or attributes in other modules, unless those are exported through [outputs](/docs/configuration/outputs.html). These outputs can be referenced in other places in your configuration, for example:
```hcl ```hcl
resource "aws_instance" "client" { resource "aws_instance" "client" {