Merge pull request #6596 from hashicorp/merge-dev-0.7

Merge rebased dev-0.7 branch into master
This commit is contained in:
James Nugent 2016-05-10 16:13:20 -04:00
commit 4faa6b37e4
143 changed files with 8808 additions and 2964 deletions

View File

@ -2,12 +2,18 @@
BACKWARDS INCOMPATIBILITIES / NOTES:
* Terraform's built-in plugins are now distributed as part of the main Terraform binary, and use the go-plugin framework. Overrides are still available using separate binaries, but will need recompiling against Terraform 0.7.
* The `concat()` interpolation function can no longer be used to join strings.
FEATURES:
* **New command:** `terraform state` to provide access to a variety of state manipulation functions [GH-5811]
* core: Lists and maps can now be used as first class types for variables, and may be passed between modules [GH-6322]
IMPROVEMENTS:
* provider/clc: Fix optional server password [GH-6414]
* provider/clc: Add support for hyperscale and bareMetal server types and package installation
* provider/clc: Fix optional server password [GH-6414]
* provider/clc: Add support for hyperscale and bareMetal server types and package installation
BUG FIXES:

20
Godeps/Godeps.json generated
View File

@ -716,6 +716,10 @@
"ImportPath": "github.com/hashicorp/go-multierror",
"Rev": "d30f09973e19c1dfcd120b2d9c4f168e68d6b5d5"
},
{
"ImportPath": "github.com/hashicorp/go-plugin",
"Rev": "cccb4a1328abbb89898f3ecf4311a05bddc4de6d"
},
{
"ImportPath": "github.com/hashicorp/go-retryablehttp",
"Rev": "5ec125ef739293cb4d57c3456dd92ba9af29ed6e"
@ -778,11 +782,11 @@
},
{
"ImportPath": "github.com/hashicorp/hil",
"Rev": "0640fefa3817883b16b77bf760c4c3a6f2589545"
"Rev": "01dc167cd239b7ccab78a683b866536cd5904719"
},
{
"ImportPath": "github.com/hashicorp/hil/ast",
"Rev": "0640fefa3817883b16b77bf760c4c3a6f2589545"
"Rev": "01dc167cd239b7ccab78a683b866536cd5904719"
},
{
"ImportPath": "github.com/hashicorp/logutils",
@ -958,7 +962,7 @@
},
{
"ImportPath": "github.com/mitchellh/cli",
"Rev": "cb6853d606ea4a12a15ac83cc43503df99fd28fb"
"Rev": "83f97d41cf100ee5f33944a8815c167d5e4aa272"
},
{
"ImportPath": "github.com/mitchellh/cloudflare-go",
@ -1229,6 +1233,16 @@
"Comment": "v1.0.0-884-gc54bbac",
"Rev": "c54bbac81d19eb4df3ad167764dbb6ff2e7194de"
},
{
"ImportPath": "github.com/ryanuber/columnize",
"Comment": "v2.0.1-8-g983d3a5",
"Rev": "983d3a5fab1bf04d1b412465d2d9f8430e2e917e"
},
{
"ImportPath": "github.com/ryanuber/columnize",
"Comment": "v2.0.1-8-g983d3a5",
"Rev": "983d3a5fab1bf04d1b412465d2d9f8430e2e917e"
},
{
"ImportPath": "github.com/satori/go.uuid",
"Rev": "d41af8bb6a7704f00bc3b7cba9355ae6a5a80048"

View File

@ -19,11 +19,11 @@ quickdev: generate
# changes will require a rebuild of everything, in which case the dev
# target should be used.
core-dev: generate
go install github.com/hashicorp/terraform
go install -tags 'core' github.com/hashicorp/terraform
# Shorthand for quickly testing the core of Terraform (i.e. "not providers")
core-test: generate
@echo "Testing core packages..." && go test $(shell go list ./... | grep -v -E 'builtin|vendor')
@echo "Testing core packages..." && go test -tags 'core' $(shell go list ./... | grep -v -E 'builtin|vendor')
# Shorthand for building and installing just one plugin for local testing.
# Run as (for example): make plugin-dev PLUGIN=provider-aws
@ -77,6 +77,7 @@ generate:
go get -u golang.org/x/tools/cmd/stringer; \
fi
go generate $$(go list ./... | grep -v /vendor/)
@go fmt command/internal_plugin_list.go > /dev/null
fmt:
gofmt -w .

View File

@ -60,7 +60,7 @@ func resourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
return err
}
var outputs map[string]string
var outputs map[string]interface{}
if !state.State().Empty() {
outputs = state.State().RootModule().Outputs
}

View File

@ -50,7 +50,13 @@ EOT
}
`, testPrivateKey),
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["key_pem"]
gotUntyped := s.RootModule().Outputs["key_pem"]
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"key_pem\" is not a string")
}
if !strings.HasPrefix(got, "-----BEGIN CERTIFICATE REQUEST----") {
return fmt.Errorf("key is missing CSR PEM preamble")
}

View File

@ -47,7 +47,11 @@ EOT
}
`, testCertRequest, testCACert, testCAPrivateKey),
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["cert_pem"]
gotUntyped := s.RootModule().Outputs["cert_pem"]
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"cert_pem\" is not a string")
}
if !strings.HasPrefix(got, "-----BEGIN CERTIFICATE----") {
return fmt.Errorf("key is missing cert PEM preamble")
}

View File

@ -29,7 +29,12 @@ func TestPrivateKeyRSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotPrivate := s.RootModule().Outputs["private_key_pem"]
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"]
gotPrivate, ok := gotPrivateUntyped.(string)
if !ok {
return fmt.Errorf("output for \"private_key_pem\" is not a string")
}
if !strings.HasPrefix(gotPrivate, "-----BEGIN RSA PRIVATE KEY----") {
return fmt.Errorf("private key is missing RSA key PEM preamble")
}
@ -37,12 +42,20 @@ func TestPrivateKeyRSA(t *testing.T) {
return fmt.Errorf("private key PEM looks too long for a 2048-bit key (got %v characters)", len(gotPrivate))
}
gotPublic := s.RootModule().Outputs["public_key_pem"]
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"]
gotPublic, ok := gotPublicUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_pem\" is not a string")
}
if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") {
return fmt.Errorf("public key is missing public key PEM preamble")
}
gotPublicSSH := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSH, ok := gotPublicSSHUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_openssh\" is not a string")
}
if !strings.HasPrefix(gotPublicSSH, "ssh-rsa ") {
return fmt.Errorf("SSH public key is missing ssh-rsa prefix")
}
@ -61,7 +74,11 @@ func TestPrivateKeyRSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["key_pem"]
gotUntyped := s.RootModule().Outputs["key_pem"]
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"key_pem\" is not a string")
}
if !strings.HasPrefix(got, "-----BEGIN RSA PRIVATE KEY----") {
return fmt.Errorf("key is missing RSA key PEM preamble")
}
@ -95,12 +112,22 @@ func TestPrivateKeyECDSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotPrivate := s.RootModule().Outputs["private_key_pem"]
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"]
gotPrivate, ok := gotPrivateUntyped.(string)
if !ok {
return fmt.Errorf("output for \"private_key_pem\" is not a string")
}
if !strings.HasPrefix(gotPrivate, "-----BEGIN EC PRIVATE KEY----") {
return fmt.Errorf("Private key is missing EC key PEM preamble")
}
gotPublic := s.RootModule().Outputs["public_key_pem"]
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"]
gotPublic, ok := gotPublicUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_pem\" is not a string")
}
if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") {
return fmt.Errorf("public key is missing public key PEM preamble")
}
@ -130,17 +157,29 @@ func TestPrivateKeyECDSA(t *testing.T) {
}
`,
Check: func(s *terraform.State) error {
gotPrivate := s.RootModule().Outputs["private_key_pem"]
gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"]
gotPrivate, ok := gotPrivateUntyped.(string)
if !ok {
return fmt.Errorf("output for \"private_key_pem\" is not a string")
}
if !strings.HasPrefix(gotPrivate, "-----BEGIN EC PRIVATE KEY----") {
return fmt.Errorf("Private key is missing EC key PEM preamble")
}
gotPublic := s.RootModule().Outputs["public_key_pem"]
gotPublicUntyped := s.RootModule().Outputs["public_key_pem"]
gotPublic, ok := gotPublicUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_pem\" is not a string")
}
if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") {
return fmt.Errorf("public key is missing public key PEM preamble")
}
gotPublicSSH := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"]
gotPublicSSH, ok := gotPublicSSHUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_openssh\" is not a string")
}
if !strings.HasPrefix(gotPublicSSH, "ecdsa-sha2-nistp256 ") {
return fmt.Errorf("P256 SSH public key is missing ecdsa prefix")
}

View File

@ -60,7 +60,12 @@ EOT
}
`, testPrivateKey),
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["key_pem"]
gotUntyped := s.RootModule().Outputs["key_pem"]
got, ok := gotUntyped.(string)
if !ok {
return fmt.Errorf("output for \"public_key_openssh\" is not a string")
}
if !strings.HasPrefix(got, "-----BEGIN CERTIFICATE----") {
return fmt.Errorf("key is missing cert PEM preamble")
}

View File

@ -251,7 +251,7 @@ func (c *ApplyCommand) Run(args []string) int {
}
if !c.Destroy {
if outputs := outputsAsString(state, ctx.Module().Config().Outputs); outputs != "" {
if outputs := outputsAsString(state, ctx.Module().Config().Outputs, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs))
}
}
@ -377,7 +377,7 @@ Options:
return strings.TrimSpace(helpText)
}
func outputsAsString(state *terraform.State, schema []*config.Output) string {
func outputsAsString(state *terraform.State, schema []*config.Output, includeHeader bool) string {
if state == nil {
return ""
}
@ -386,37 +386,44 @@ func outputsAsString(state *terraform.State, schema []*config.Output) string {
outputBuf := new(bytes.Buffer)
if len(outputs) > 0 {
schemaMap := make(map[string]*config.Output)
for _, s := range schema {
schemaMap[s.Name] = s
if schema != nil {
for _, s := range schema {
schemaMap[s.Name] = s
}
}
outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
if includeHeader {
outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
}
// Output the outputs in alphabetical order
keyLen := 0
keys := make([]string, 0, len(outputs))
ks := make([]string, 0, len(outputs))
for key, _ := range outputs {
keys = append(keys, key)
ks = append(ks, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(keys)
sort.Strings(ks)
for _, k := range ks {
schema, ok := schemaMap[k]
if ok && schema.Sensitive {
outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
continue
}
for _, k := range keys {
v := outputs[k]
if schemaMap[k].Sensitive {
outputBuf.WriteString(fmt.Sprintf(
" %s%s = <sensitive>\n",
k,
strings.Repeat(" ", keyLen-len(k))))
} else {
outputBuf.WriteString(fmt.Sprintf(
" %s%s = %s\n",
k,
strings.Repeat(" ", keyLen-len(k)),
v))
switch typedV := v.(type) {
case string:
outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
case []interface{}:
outputBuf.WriteString(formatListOutput("", k, typedV))
outputBuf.WriteString("\n")
case map[string]interface{}:
outputBuf.WriteString(formatMapOutput("", k, typedV))
outputBuf.WriteString("\n")
}
}
}

View File

@ -887,8 +887,6 @@ func TestApply_stateNoExist(t *testing.T) {
}
func TestApply_sensitiveOutput(t *testing.T) {
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
@ -898,6 +896,8 @@ func TestApply_sensitiveOutput(t *testing.T) {
},
}
statePath := testTempFile(t)
args := []string{
"-state", statePath,
testFixturePath("apply-sensitive-output"),
@ -911,11 +911,75 @@ func TestApply_sensitiveOutput(t *testing.T) {
if !strings.Contains(output, "notsensitive = Hello world") {
t.Fatalf("bad: output should contain 'notsensitive' output\n%s", output)
}
if !strings.Contains(output, "sensitive = <sensitive>") {
if !strings.Contains(output, "sensitive = <sensitive>") {
t.Fatalf("bad: output should contain 'sensitive' output\n%s", output)
}
}
func TestApply_stateFuture(t *testing.T) {
originalState := testState()
originalState.TFVersion = "99.99.99"
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
testFixturePath("apply"),
}
if code := c.Run(args); code == 0 {
t.Fatal("should fail")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
if !newState.Equal(originalState) {
t.Fatalf("bad: %#v", newState)
}
if newState.TFVersion != originalState.TFVersion {
t.Fatalf("bad: %#v", newState)
}
}
func TestApply_statePast(t *testing.T) {
originalState := testState()
originalState.TFVersion = "0.1.0"
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
testFixturePath("apply"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestApply_vars(t *testing.T) {
statePath := testTempFile(t)

View File

@ -0,0 +1,90 @@
package command
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/plugin"
"github.com/kardianos/osext"
)
// InternalPluginCommand is a Command implementation that allows plugins to be
// compiled into the main Terraform binary and executed via a subcommand.
type InternalPluginCommand struct {
Meta
}
const TFSPACE = "-TFSPACE-"
// BuildPluginCommandString builds a special string for executing internal
// plugins. It has the following format:
//
// /path/to/terraform-TFSPACE-internal-plugin-TFSPACE-terraform-provider-aws
//
// We split the string on -TFSPACE- to build the command executor. The reason we
// use -TFSPACE- is so we can support spaces in the /path/to/terraform part.
func BuildPluginCommandString(pluginType, pluginName string) (string, error) {
terraformPath, err := osext.Executable()
if err != nil {
return "", err
}
parts := []string{terraformPath, "internal-plugin", pluginType, pluginName}
return strings.Join(parts, TFSPACE), nil
}
func (c *InternalPluginCommand) Run(args []string) int {
if len(args) != 2 {
log.Printf("Wrong number of args; expected: terraform internal-plugin pluginType pluginName")
return 1
}
pluginType := args[0]
pluginName := args[1]
log.SetPrefix(fmt.Sprintf("%s-%s (internal) ", pluginName, pluginType))
switch pluginType {
case "provider":
pluginFunc, found := InternalProviders[pluginName]
if !found {
log.Printf("[ERROR] Could not load provider: %s", pluginName)
return 1
}
log.Printf("[INFO] Starting provider plugin %s", pluginName)
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: pluginFunc,
})
case "provisioner":
pluginFunc, found := InternalProvisioners[pluginName]
if !found {
log.Printf("[ERROR] Could not load provisioner: %s", pluginName)
return 1
}
log.Printf("[INFO] Starting provisioner plugin %s", pluginName)
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: pluginFunc,
})
default:
log.Printf("[ERROR] Invalid plugin type %s", pluginType)
return 1
}
return 0
}
func (c *InternalPluginCommand) Help() string {
helpText := `
Usage: terraform internal-plugin pluginType pluginName
Runs an internally-compiled version of a plugin from the terraform binary.
NOTE: this is an internal command and you should not call it yourself.
`
return strings.TrimSpace(helpText)
}
func (c *InternalPluginCommand) Synopsis() string {
return "internal plugin command"
}

View File

@ -0,0 +1,13 @@
// +build core
// This file is included whenever the 'core' build tag is specified. This is
// used by make core-dev and make core-test to compile a build significantly
// more quickly, but it will not include any provider or provisioner plugins.
package command
import "github.com/hashicorp/terraform/plugin"
var InternalProviders = map[string]plugin.ProviderFunc{}
var InternalProvisioners = map[string]plugin.ProvisionerFunc{}

View File

@ -0,0 +1,106 @@
// +build !core
//
// This file is automatically generated by scripts/generate-plugins.go -- Do not edit!
//
package command
import (
atlasprovider "github.com/hashicorp/terraform/builtin/providers/atlas"
awsprovider "github.com/hashicorp/terraform/builtin/providers/aws"
azureprovider "github.com/hashicorp/terraform/builtin/providers/azure"
azurermprovider "github.com/hashicorp/terraform/builtin/providers/azurerm"
chefprovider "github.com/hashicorp/terraform/builtin/providers/chef"
clcprovider "github.com/hashicorp/terraform/builtin/providers/clc"
cloudflareprovider "github.com/hashicorp/terraform/builtin/providers/cloudflare"
cloudstackprovider "github.com/hashicorp/terraform/builtin/providers/cloudstack"
cobblerprovider "github.com/hashicorp/terraform/builtin/providers/cobbler"
consulprovider "github.com/hashicorp/terraform/builtin/providers/consul"
datadogprovider "github.com/hashicorp/terraform/builtin/providers/datadog"
digitaloceanprovider "github.com/hashicorp/terraform/builtin/providers/digitalocean"
dmeprovider "github.com/hashicorp/terraform/builtin/providers/dme"
dnsimpleprovider "github.com/hashicorp/terraform/builtin/providers/dnsimple"
dockerprovider "github.com/hashicorp/terraform/builtin/providers/docker"
dynprovider "github.com/hashicorp/terraform/builtin/providers/dyn"
fastlyprovider "github.com/hashicorp/terraform/builtin/providers/fastly"
githubprovider "github.com/hashicorp/terraform/builtin/providers/github"
googleprovider "github.com/hashicorp/terraform/builtin/providers/google"
herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku"
influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb"
libratoprovider "github.com/hashicorp/terraform/builtin/providers/librato"
mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun"
mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql"
nullprovider "github.com/hashicorp/terraform/builtin/providers/null"
openstackprovider "github.com/hashicorp/terraform/builtin/providers/openstack"
packetprovider "github.com/hashicorp/terraform/builtin/providers/packet"
postgresqlprovider "github.com/hashicorp/terraform/builtin/providers/postgresql"
powerdnsprovider "github.com/hashicorp/terraform/builtin/providers/powerdns"
rundeckprovider "github.com/hashicorp/terraform/builtin/providers/rundeck"
softlayerprovider "github.com/hashicorp/terraform/builtin/providers/softlayer"
statuscakeprovider "github.com/hashicorp/terraform/builtin/providers/statuscake"
templateprovider "github.com/hashicorp/terraform/builtin/providers/template"
terraformprovider "github.com/hashicorp/terraform/builtin/providers/terraform"
testprovider "github.com/hashicorp/terraform/builtin/providers/test"
tlsprovider "github.com/hashicorp/terraform/builtin/providers/tls"
tritonprovider "github.com/hashicorp/terraform/builtin/providers/triton"
ultradnsprovider "github.com/hashicorp/terraform/builtin/providers/ultradns"
vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd"
vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere"
chefresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef"
fileresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
localexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
remoteexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)
var InternalProviders = map[string]plugin.ProviderFunc{
"atlas": atlasprovider.Provider,
"aws": awsprovider.Provider,
"azure": azureprovider.Provider,
"azurerm": azurermprovider.Provider,
"chef": chefprovider.Provider,
"clc": clcprovider.Provider,
"cloudflare": cloudflareprovider.Provider,
"cloudstack": cloudstackprovider.Provider,
"cobbler": cobblerprovider.Provider,
"consul": consulprovider.Provider,
"datadog": datadogprovider.Provider,
"digitalocean": digitaloceanprovider.Provider,
"dme": dmeprovider.Provider,
"dnsimple": dnsimpleprovider.Provider,
"docker": dockerprovider.Provider,
"dyn": dynprovider.Provider,
"fastly": fastlyprovider.Provider,
"github": githubprovider.Provider,
"google": googleprovider.Provider,
"heroku": herokuprovider.Provider,
"influxdb": influxdbprovider.Provider,
"librato": libratoprovider.Provider,
"mailgun": mailgunprovider.Provider,
"mysql": mysqlprovider.Provider,
"null": nullprovider.Provider,
"openstack": openstackprovider.Provider,
"packet": packetprovider.Provider,
"postgresql": postgresqlprovider.Provider,
"powerdns": powerdnsprovider.Provider,
"rundeck": rundeckprovider.Provider,
"softlayer": softlayerprovider.Provider,
"statuscake": statuscakeprovider.Provider,
"template": templateprovider.Provider,
"terraform": terraformprovider.Provider,
"test": testprovider.Provider,
"tls": tlsprovider.Provider,
"triton": tritonprovider.Provider,
"ultradns": ultradnsprovider.Provider,
"vcd": vcdprovider.Provider,
"vsphere": vsphereprovider.Provider,
}
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
"chef": func() terraform.ResourceProvisioner { return new(chefresourceprovisioner.ResourceProvisioner) },
"file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) },
"local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) },
"remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) },
}

View File

@ -0,0 +1,34 @@
// +build !core
package command
import "testing"
func TestInternalPlugin_InternalProviders(t *testing.T) {
// Note this is a randomish sample and does not check for all plugins
for _, name := range []string{"atlas", "consul", "docker", "template"} {
if _, ok := InternalProviders[name]; !ok {
t.Errorf("Expected to find %s in InternalProviders", name)
}
}
}
func TestInternalPlugin_InternalProvisioners(t *testing.T) {
for _, name := range []string{"chef", "file", "local-exec", "remote-exec"} {
if _, ok := InternalProvisioners[name]; !ok {
t.Errorf("Expected to find %s in InternalProvisioners", name)
}
}
}
func TestInternalPlugin_BuildPluginCommandString(t *testing.T) {
actual, err := BuildPluginCommandString("provisioner", "remote-exec")
if err != nil {
t.Fatalf(err.Error())
}
expected := "-TFSPACE-internal-plugin-TFSPACE-provisioner-TFSPACE-remote-exec"
if actual[len(actual)-len(expected):] != expected {
t.Errorf("Expected command to end with %s; got:\n%s\n", expected, actual)
}
}

View File

@ -126,7 +126,8 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
"variable values, create a new plan file.")
}
return plan.Context(opts), true, nil
ctx, err := plan.Context(opts)
return ctx, true, err
}
}
@ -158,8 +159,8 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
opts.Module = mod
opts.Parallelism = copts.Parallelism
opts.State = state.State()
ctx := terraform.NewContext(opts)
return ctx, false, nil
ctx, err := terraform.NewContext(opts)
return ctx, false, err
}
// DataDir returns the directory where local data will be stored.
@ -326,6 +327,9 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
}()
f.SetOutput(errW)
// Set the default Usage to empty
f.Usage = func() {}
return f
}

View File

@ -1,9 +1,11 @@
package command
import (
"bytes"
"flag"
"fmt"
"sort"
"strconv"
"strings"
)
@ -27,7 +29,7 @@ func (c *OutputCommand) Run(args []string) int {
}
args = cmdFlags.Args()
if len(args) > 1 {
if len(args) > 2 {
c.Ui.Error(
"The output command expects exactly one argument with the name\n" +
"of an output variable or no arguments to show all outputs.\n")
@ -40,6 +42,11 @@ func (c *OutputCommand) Run(args []string) int {
name = args[0]
}
index := ""
if len(args) > 1 {
index = args[1]
}
stateStore, err := c.Meta.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
@ -74,17 +81,7 @@ func (c *OutputCommand) Run(args []string) int {
}
if name == "" {
ks := make([]string, 0, len(mod.Outputs))
for k, _ := range mod.Outputs {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
v := mod.Outputs[k]
c.Ui.Output(fmt.Sprintf("%s = %s", k, v))
}
c.Ui.Output(outputsAsString(state, nil, false))
return 0
}
@ -98,10 +95,101 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
c.Ui.Output(v)
switch output := v.(type) {
case string:
c.Ui.Output(output)
return 0
case []interface{}:
if index == "" {
c.Ui.Output(formatListOutput("", "", output))
break
}
indexInt, err := strconv.Atoi(index)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"The index %q requested is not valid for the list output\n"+
"%q - indices must be numeric, and in the range 0-%d", index, name,
len(output)-1))
break
}
if indexInt < 0 || indexInt >= len(output) {
c.Ui.Error(fmt.Sprintf(
"The index %d requested is not valid for the list output\n"+
"%q - indices must be in the range 0-%d", indexInt, name,
len(output)-1))
break
}
c.Ui.Output(fmt.Sprintf("%s", output[indexInt]))
return 0
case map[string]interface{}:
if index == "" {
c.Ui.Output(formatMapOutput("", "", output))
break
}
if value, ok := output[index]; ok {
c.Ui.Output(fmt.Sprintf("%s", value))
return 0
} else {
return 1
}
default:
panic(fmt.Errorf("Unknown output type: %T", output))
}
return 0
}
func formatListOutput(indent, outputName string, outputList []interface{}) string {
keyIndent := ""
outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = [", indent, outputName))
keyIndent = " "
}
for _, value := range outputList {
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value))
}
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("\n%s]", indent))
}
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {
ks = append(ks, k)
}
sort.Strings(ks)
keyIndent := ""
outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = {", indent, outputName))
keyIndent = " "
}
for _, k := range ks {
v := outputMap[k]
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s = %v", indent, keyIndent, k, v))
}
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("\n%s}", indent))
}
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func (c *OutputCommand) Help() string {
helpText := `
Usage: terraform output [options] [NAME]

View File

@ -16,7 +16,7 @@ func TestOutput(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},
@ -52,13 +52,13 @@ func TestModuleOutput(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},
&terraform.ModuleState{
Path: []string{"root", "my_module"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"blah": "tastatur",
},
},
@ -96,7 +96,7 @@ func TestMissingModuleOutput(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},
@ -129,7 +129,7 @@ func TestOutput_badVar(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},
@ -160,7 +160,7 @@ func TestOutput_blank(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
"name": "john-doe",
},
@ -253,7 +253,7 @@ func TestOutput_noVars(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{},
Outputs: map[string]interface{}{},
},
},
}
@ -282,7 +282,7 @@ func TestOutput_stateDefault(t *testing.T) {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},

View File

@ -345,6 +345,70 @@ func TestPlan_stateDefault(t *testing.T) {
}
}
func TestPlan_stateFuture(t *testing.T) {
originalState := testState()
originalState.TFVersion = "99.99.99"
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
testFixturePath("plan"),
}
if code := c.Run(args); code == 0 {
t.Fatal("should fail")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
if !newState.Equal(originalState) {
t.Fatalf("bad: %#v", newState)
}
if newState.TFVersion != originalState.TFVersion {
t.Fatalf("bad: %#v", newState)
}
}
func TestPlan_statePast(t *testing.T) {
originalState := testState()
originalState.TFVersion = "0.1.0"
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
testFixturePath("plan"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestPlan_vars(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)

View File

@ -109,7 +109,7 @@ func (c *RefreshCommand) Run(args []string) int {
return 1
}
if outputs := outputsAsString(newState, ctx.Module().Config().Outputs); outputs != "" {
if outputs := outputsAsString(newState, ctx.Module().Config().Outputs, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs))
}

View File

@ -221,6 +221,109 @@ func TestRefresh_defaultState(t *testing.T) {
}
}
func TestRefresh_futureState(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("refresh")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
state := testState()
state.TFVersion = "99.99.99"
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code == 0 {
t.Fatal("should fail")
}
if p.RefreshCalled {
t.Fatal("refresh should not be called")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(newState.String())
expected := strings.TrimSpace(state.String())
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestRefresh_pastState(t *testing.T) {
state := testState()
state.TFVersion = "0.1.0"
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
"-state", statePath,
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(newState.String())
expected := strings.TrimSpace(testRefreshStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
if newState.TFVersion != terraform.Version {
t.Fatalf("bad:\n\n%s", newState.TFVersion)
}
}
func TestRefresh_outPath(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)

40
command/state_command.go Normal file
View File

@ -0,0 +1,40 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
// StateCommand is a Command implementation that just shows help for
// the subcommands nested below it.
type StateCommand struct {
Meta
}
func (c *StateCommand) Run(args []string) int {
return cli.RunResultHelp
}
func (c *StateCommand) Help() string {
helpText := `
Usage: terraform state <subcommand> [options] [args]
This command has subcommands for advanced state management.
These subcommands can be used to slice and dice the Terraform state.
This is sometimes necessary in advanced cases. For your safety, all
state management commands that modify the state create a timestamped
backup of the state prior to making modifications.
The structure and output of the commands is specifically tailored to work
well with the common Unix utilities such as grep, awk, etc. We recommend
using those tools to perform more advanced state tasks.
`
return strings.TrimSpace(helpText)
}
func (c *StateCommand) Synopsis() string {
return "Advanced state management"
}

101
command/state_list.go Normal file
View File

@ -0,0 +1,101 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// StateListCommand is a Command implementation that lists the resources
// within a state file.
type StateListCommand struct {
Meta
}
func (c *StateListCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("state list")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
args = cmdFlags.Args()
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
}
stateReal := state.State()
if stateReal == nil {
c.Ui.Error(fmt.Sprintf(errStateNotFound))
return 1
}
filter := &terraform.StateFilter{State: stateReal}
results, err := filter.Filter(args...)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateFilter, err))
return cli.RunResultHelp
}
for _, result := range results {
if _, ok := result.Value.(*terraform.InstanceState); ok {
c.Ui.Output(result.Address)
}
}
return 0
}
func (c *StateListCommand) Help() string {
helpText := `
Usage: terraform state list [options] [pattern...]
List resources in the Terraform state.
This command lists resources in the Terraform state. The pattern argument
can be used to filter the resources by resource or module. If no pattern
is given, all resources are listed.
The pattern argument is meant to provide very simple filtering. For
advanced filtering, please use tools such as "grep". The output of this
command is designed to be friendly for this usage.
The pattern argument accepts any resource targeting syntax. Please
refer to the documentation on resource targeting syntax for more
information.
Options:
-state=statefile Path to a Terraform state file to use to look
up Terraform-managed resources. By default it will
use the state "terraform.tfstate" if it exists.
`
return strings.TrimSpace(helpText)
}
func (c *StateListCommand) Synopsis() string {
return "List resources in the state"
}
const errStateFilter = `Error filtering state: %[1]s
Please ensure that all your addresses are formatted properly.`
const errStateLoadingState = `Error loading the state: %[1]s
Please ensure that your Terraform state exists and that you've
configured it properly. You can use the "-state" flag to point
Terraform at another state file.`
const errStateNotFound = `No state file was found!
State management commands require a state file. Run this command
in a directory where Terraform has been run or use the -state flag
to point the command to a specific state location.`

View File

@ -0,0 +1,59 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestStateList(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &StateListCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test that outputs were displayed
expected := strings.TrimSpace(testStateListOutput) + "\n"
actual := ui.OutputWriter.String()
if actual != expected {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
}
}
func TestStateList_noState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &StateListCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
const testStateListOutput = `
test_instance.foo
`

34
command/state_meta.go Normal file
View File

@ -0,0 +1,34 @@
package command
import (
"errors"
"github.com/hashicorp/terraform/terraform"
)
// StateMeta is the meta struct that should be embedded in state subcommands.
type StateMeta struct{}
// filterInstance filters a single instance out of filter results.
func (c *StateMeta) filterInstance(rs []*terraform.StateFilterResult) (*terraform.StateFilterResult, error) {
var result *terraform.StateFilterResult
for _, r := range rs {
if _, ok := r.Value.(*terraform.InstanceState); !ok {
continue
}
if result != nil {
return nil, errors.New(errStateMultiple)
}
result = r
}
return result, nil
}
const errStateMultiple = `Multiple instances found for the given pattern!
This command requires that the pattern match exactly one instance
of a resource. To view the matched instances, use "terraform state list".
Please modify the pattern to match only a single instance.`

100
command/state_show.go Normal file
View File

@ -0,0 +1,100 @@
package command
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/ryanuber/columnize"
)
// StateShowCommand is a Command implementation that shows a single resource.
type StateShowCommand struct {
Meta
StateMeta
}
func (c *StateShowCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("state show")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
args = cmdFlags.Args()
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
}
stateReal := state.State()
if stateReal == nil {
c.Ui.Error(fmt.Sprintf(errStateNotFound))
return 1
}
filter := &terraform.StateFilter{State: stateReal}
results, err := filter.Filter(args...)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateFilter, err))
return 1
}
instance, err := c.filterInstance(results)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
is := instance.Value.(*terraform.InstanceState)
// Sort the keys
keys := make([]string, 0, len(is.Attributes))
for k, _ := range is.Attributes {
keys = append(keys, k)
}
sort.Strings(keys)
// Build the output
output := make([]string, 0, len(is.Attributes)+1)
output = append(output, fmt.Sprintf("id | %s", is.ID))
for _, k := range keys {
if k != "id" {
output = append(output, fmt.Sprintf("%s | %s", k, is.Attributes[k]))
}
}
// Output
config := columnize.DefaultConfig()
config.Glue = " = "
c.Ui.Output(columnize.Format(output, config))
return 0
}
func (c *StateShowCommand) Help() string {
helpText := `
Usage: terraform state show [options] ADDRESS
Shows the attributes of a resource in the Terraform state.
This command shows the attributes of a single resource in the Terraform
state. The address argument must be used to specify a single resource.
You can view the list of available resources with "terraform state list".
Options:
-state=statefile Path to a Terraform state file to use to look
up Terraform-managed resources. By default it will
use the state "terraform.tfstate" if it exists.
`
return strings.TrimSpace(helpText)
}
func (c *StateShowCommand) Synopsis() string {
return "Show a resource in the state"
}

133
command/state_show_test.go Normal file
View File

@ -0,0 +1,133 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestStateShow(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test that outputs were displayed
expected := strings.TrimSpace(testStateShowOutput) + "\n"
actual := ui.OutputWriter.String()
if actual != expected {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
}
}
func TestStateShow_multi(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo.0": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
"test_instance.foo.1": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestStateShow_noState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
const testStateShowOutput = `
id = bar
bar = value
foo = value
`

View File

@ -10,6 +10,7 @@ import (
// Commands is the mapping of all the available Terraform commands.
var Commands map[string]cli.CommandFactory
var PlumbingCommands map[string]struct{}
// Ui is the cli.Ui used for communicating to the outside world.
var Ui cli.Ui
@ -34,6 +35,10 @@ func init() {
Ui: Ui,
}
PlumbingCommands = map[string]struct{}{
"state": struct{}{}, // includes all subcommands
}
Commands = map[string]cli.CommandFactory{
"apply": func() (cli.Command, error) {
return &command.ApplyCommand{
@ -74,6 +79,12 @@ func init() {
}, nil
},
"internal-plugin": func() (cli.Command, error) {
return &command.InternalPluginCommand{
Meta: meta,
}, nil
},
"output": func() (cli.Command, error) {
return &command.OutputCommand{
Meta: meta,
@ -137,6 +148,28 @@ func init() {
Meta: meta,
}, nil
},
//-----------------------------------------------------------
// Plumbing
//-----------------------------------------------------------
"state": func() (cli.Command, error) {
return &command.StateCommand{
Meta: meta,
}, nil
},
"state list": func() (cli.Command, error) {
return &command.StateListCommand{
Meta: meta,
}, nil
},
"state show": func() (cli.Command, error) {
return &command.StateShowCommand{
Meta: meta,
}, nil
},
}
}

View File

@ -1,3 +1,4 @@
//go:generate go run ./scripts/generate-plugins.go
package main
import (
@ -9,10 +10,13 @@ import (
"path/filepath"
"strings"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/command"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
"github.com/kardianos/osext"
"github.com/mitchellh/cli"
)
// Config is the structure of the configuration for the Terraform CLI.
@ -73,18 +77,22 @@ func LoadConfig(path string) (*Config, error) {
return &result, nil
}
// Discover discovers plugins.
// Discover plugins located on disk, and fall back on plugins baked into the
// Terraform binary.
//
// This looks in the directory of the executable and the CWD, in that
// order for priority.
func (c *Config) Discover() error {
// Look in the cwd.
if err := c.discover("."); err != nil {
return err
}
// Look in the plugins directory. This will override any found
// in the current directory.
// We look in the following places for plugins:
//
// 1. Terraform configuration path
// 2. Path where Terraform is installed
// 3. Path where Terraform is invoked
//
// Whichever file is discoverd LAST wins.
//
// Finally, we look at the list of plugins compiled into Terraform. If any of
// them has not been found on disk we use the internal version. This allows
// users to add / replace plugins without recompiling the main binary.
func (c *Config) Discover(ui cli.Ui) error {
// Look in ~/.terraform.d/plugins/
dir, err := ConfigDir()
if err != nil {
log.Printf("[ERR] Error loading config directory: %s", err)
@ -94,8 +102,8 @@ func (c *Config) Discover() error {
}
}
// Next, look in the same directory as the executable. Any conflicts
// will overwrite those found in our current directory.
// Next, look in the same directory as the Terraform executable, usually
// /usr/local/bin. If found, this replaces what we found in the config path.
exePath, err := osext.Executable()
if err != nil {
log.Printf("[ERR] Error loading exe directory: %s", err)
@ -105,6 +113,42 @@ func (c *Config) Discover() error {
}
}
// Finally look in the cwd (where we are invoke Terraform). If found, this
// replaces anything we found in the config / install paths.
if err := c.discover("."); err != nil {
return err
}
// Finally, if we have a plugin compiled into Terraform and we didn't find
// a replacement on disk, we'll just use the internal version.
for name, _ := range command.InternalProviders {
if path, found := c.Providers[name]; found {
ui.Warn(fmt.Sprintf("[WARN] %s overrides an internal plugin for %s-provider.\n"+
" If you did not expect to see this message you will need to remove the old plugin.\n"+
" See https://www.terraform.io/docs/internals/internal-plugins.html", path, name))
} else {
cmd, err := command.BuildPluginCommandString("provider", name)
if err != nil {
return err
}
c.Providers[name] = cmd
}
}
for name, _ := range command.InternalProvisioners {
if path, found := c.Provisioners[name]; found {
ui.Warn(fmt.Sprintf("[WARN] %s overrides an internal plugin for %s-provisioner.\n"+
" If you did not expect to see this message you will need to remove the old plugin.\n"+
" See https://www.terraform.io/docs/internals/internal-plugins.html", path, name))
} else {
cmd, err := command.BuildPluginCommandString("provisioner", name)
if err != nil {
return err
}
c.Provisioners[name] = cmd
}
}
return nil
}
@ -202,7 +246,9 @@ func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory
// Build the plugin client configuration and init the plugin
var config plugin.ClientConfig
config.Cmd = pluginCmd(path)
config.HandshakeConfig = tfplugin.Handshake
config.Managed = true
config.Plugins = tfplugin.PluginMap
client := plugin.NewClient(&config)
return func() (terraform.ResourceProvider, error) {
@ -213,7 +259,12 @@ func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory
return nil, err
}
return rpcClient.ResourceProvider()
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvider), nil
}
}
@ -232,8 +283,10 @@ func (c *Config) ProvisionerFactories() map[string]terraform.ResourceProvisioner
func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFactory {
// Build the plugin client configuration and init the plugin
var config plugin.ClientConfig
config.HandshakeConfig = tfplugin.Handshake
config.Cmd = pluginCmd(path)
config.Managed = true
config.Plugins = tfplugin.PluginMap
client := plugin.NewClient(&config)
return func() (terraform.ResourceProvisioner, error) {
@ -242,7 +295,12 @@ func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFa
return nil, err
}
return rpcClient.ResourceProvisioner()
raw, err := rpcClient.Dispense(tfplugin.ProvisionerPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvisioner), nil
}
}
@ -270,6 +328,12 @@ func pluginCmd(path string) *exec.Cmd {
}
}
// No plugin binary found, so try to use an internal plugin.
if strings.Contains(path, command.TFSPACE) {
parts := strings.Split(path, command.TFSPACE)
return exec.Command(parts[0], parts[1:]...)
}
// If we still don't have a path, then just set it to the original
// given path.
if cmdPath == "" {

View File

@ -11,7 +11,6 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/flatmap"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/reflectwalk"
)
@ -162,6 +161,7 @@ type VariableType byte
const (
VariableTypeUnknown VariableType = iota
VariableTypeString
VariableTypeList
VariableTypeMap
)
@ -171,6 +171,8 @@ func (v VariableType) Printable() string {
return "string"
case VariableTypeMap:
return "map"
case VariableTypeList:
return "list"
default:
return "unknown"
}
@ -239,7 +241,7 @@ func (c *Config) Validate() error {
}
interp := false
fn := func(ast.Node) (string, error) {
fn := func(ast.Node) (interface{}, error) {
interp = true
return "", nil
}
@ -352,16 +354,30 @@ func (c *Config) Validate() error {
m.Id()))
}
// Check that the configuration can all be strings
// Check that the configuration can all be strings, lists or maps
raw := make(map[string]interface{})
for k, v := range m.RawConfig.Raw {
var strVal string
if err := mapstructure.WeakDecode(v, &strVal); err != nil {
errs = append(errs, fmt.Errorf(
"%s: variable %s must be a string value",
m.Id(), k))
if err := mapstructure.WeakDecode(v, &strVal); err == nil {
raw[k] = strVal
continue
}
raw[k] = strVal
var mapVal map[string]interface{}
if err := mapstructure.WeakDecode(v, &mapVal); err == nil {
raw[k] = mapVal
continue
}
var sliceVal []interface{}
if err := mapstructure.WeakDecode(v, &sliceVal); err == nil {
raw[k] = sliceVal
continue
}
errs = append(errs, fmt.Errorf(
"%s: variable %s must be a string, list or map value",
m.Id(), k))
}
// Check for invalid count variables
@ -450,7 +466,7 @@ func (c *Config) Validate() error {
}
// Interpolate with a fixed number to verify that its a number.
r.RawCount.interpolate(func(root ast.Node) (string, error) {
r.RawCount.interpolate(func(root ast.Node) (interface{}, error) {
// Execute the node but transform the AST so that it returns
// a fixed value of "5" for all interpolations.
result, err := hil.Eval(
@ -461,7 +477,7 @@ func (c *Config) Validate() error {
return "", err
}
return result.Value.(string), nil
return result.Value, nil
})
_, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0)
if err != nil {
@ -722,7 +738,8 @@ func (c *Config) validateVarContextFn(
if rv.Multi && rv.Index == -1 {
*errs = append(*errs, fmt.Errorf(
"%s: multi-variable must be in a slice", source))
"%s: use of the splat ('*') operator must be wrapped in a list declaration",
source))
}
}
}
@ -809,28 +826,6 @@ func (r *Resource) mergerMerge(m merger) merger {
return &result
}
// DefaultsMap returns a map of default values for this variable.
func (v *Variable) DefaultsMap() map[string]string {
if v.Default == nil {
return nil
}
n := fmt.Sprintf("var.%s", v.Name)
switch v.Type() {
case VariableTypeString:
return map[string]string{n: v.Default.(string)}
case VariableTypeMap:
result := flatmap.Flatten(map[string]interface{}{
n: v.Default.(map[string]string),
})
result[n] = v.Name
return result
default:
return nil
}
}
// Merge merges two variables to create a new third variable.
func (v *Variable) Merge(v2 *Variable) *Variable {
// Shallow copy the variable
@ -852,6 +847,7 @@ func (v *Variable) Merge(v2 *Variable) *Variable {
var typeStringMap = map[string]VariableType{
"string": VariableTypeString,
"map": VariableTypeMap,
"list": VariableTypeList,
}
// Type returns the type of variable this is.
@ -911,9 +907,9 @@ func (v *Variable) inferTypeFromDefault() VariableType {
return VariableTypeString
}
var strVal string
if err := mapstructure.WeakDecode(v.Default, &strVal); err == nil {
v.Default = strVal
var s string
if err := mapstructure.WeakDecode(v.Default, &s); err == nil {
v.Default = s
return VariableTypeString
}
@ -923,5 +919,11 @@ func (v *Variable) inferTypeFromDefault() VariableType {
return VariableTypeMap
}
var l []string
if err := mapstructure.WeakDecode(v.Default, &l); err == nil {
v.Default = l
return VariableTypeList
}
return VariableTypeUnknown
}

View File

@ -2,7 +2,6 @@ package config
import (
"path/filepath"
"reflect"
"strings"
"testing"
)
@ -216,8 +215,15 @@ func TestConfigValidate_moduleVarInt(t *testing.T) {
func TestConfigValidate_moduleVarMap(t *testing.T) {
c := testConfig(t, "validate-module-var-map")
if err := c.Validate(); err == nil {
t.Fatal("should be invalid")
if err := c.Validate(); err != nil {
t.Fatalf("should be valid: %s", err)
}
}
func TestConfigValidate_moduleVarList(t *testing.T) {
c := testConfig(t, "validate-module-var-list")
if err := c.Validate(); err != nil {
t.Fatalf("should be valid: %s", err)
}
}
@ -368,10 +374,10 @@ func TestConfigValidate_varDefault(t *testing.T) {
}
}
func TestConfigValidate_varDefaultBadType(t *testing.T) {
c := testConfig(t, "validate-var-default-bad-type")
if err := c.Validate(); err == nil {
t.Fatal("should not be valid")
func TestConfigValidate_varDefaultListType(t *testing.T) {
c := testConfig(t, "validate-var-default-list-type")
if err := c.Validate(); err != nil {
t.Fatalf("should be valid: %s", err)
}
}
@ -458,43 +464,6 @@ func TestProviderConfigName(t *testing.T) {
}
}
func TestVariableDefaultsMap(t *testing.T) {
cases := []struct {
Default interface{}
Output map[string]string
}{
{
nil,
nil,
},
{
"foo",
map[string]string{"var.foo": "foo"},
},
{
map[interface{}]interface{}{
"foo": "bar",
"bar": "baz",
},
map[string]string{
"var.foo": "foo",
"var.foo.foo": "bar",
"var.foo.bar": "baz",
},
},
}
for i, tc := range cases {
v := &Variable{Name: "foo", Default: tc.Default}
actual := v.DefaultsMap()
if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("%d: bad: %#v", i, actual)
}
}
}
func testConfig(t *testing.T, name string) *Config {
c, err := LoadFile(filepath.Join(fixtureDir, name, "main.tf"))
if err != nil {

View File

@ -284,18 +284,35 @@ func DetectVariables(root ast.Node) ([]InterpolatedVariable, error) {
return n
}
vn, ok := n.(*ast.VariableAccess)
if !ok {
switch vn := n.(type) {
case *ast.VariableAccess:
v, err := NewInterpolatedVariable(vn.Name)
if err != nil {
resultErr = err
return n
}
result = append(result, v)
case *ast.Index:
if va, ok := vn.Target.(*ast.VariableAccess); ok {
v, err := NewInterpolatedVariable(va.Name)
if err != nil {
resultErr = err
return n
}
result = append(result, v)
}
if va, ok := vn.Key.(*ast.VariableAccess); ok {
v, err := NewInterpolatedVariable(va.Name)
if err != nil {
resultErr = err
return n
}
result = append(result, v)
}
default:
return n
}
v, err := NewInterpolatedVariable(vn.Name)
if err != nil {
resultErr = err
return n
}
result = append(result, v)
return n
}

View File

@ -1,7 +1,6 @@
package config
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
@ -19,10 +18,36 @@ import (
"github.com/apparentlymart/go-cidr/cidr"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/mitchellh/go-homedir"
)
// stringSliceToVariableValue converts a string slice into the value
// required to be returned from interpolation functions which return
// TypeList.
func stringSliceToVariableValue(values []string) []ast.Variable {
output := make([]ast.Variable, len(values))
for index, value := range values {
output[index] = ast.Variable{
Type: ast.TypeString,
Value: value,
}
}
return output
}
func listVariableValueToStringSlice(values []ast.Variable) ([]string, error) {
output := make([]string, len(values))
for index, value := range values {
if value.Type != ast.TypeString {
return []string{}, fmt.Errorf("list has non-string element (%T)", value.Type.String())
}
output[index] = value.Value.(string)
}
return output, nil
}
// Funcs is the mapping of built-in functions for configuration.
func Funcs() map[string]ast.Function {
return map[string]ast.Function{
@ -60,14 +85,23 @@ func Funcs() map[string]ast.Function {
// (e.g. as returned by "split") of any empty strings.
func interpolationFuncCompact() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
ReturnType: ast.TypeString,
ArgTypes: []ast.Type{ast.TypeList},
ReturnType: ast.TypeList,
Variadic: false,
Callback: func(args []interface{}) (interface{}, error) {
if !IsStringList(args[0].(string)) {
return args[0].(string), nil
inputList := args[0].([]ast.Variable)
var outputList []string
for _, val := range inputList {
if strVal, ok := val.Value.(string); ok {
if strVal == "" {
continue
}
outputList = append(outputList, strVal)
}
}
return StringList(args[0].(string)).Compact().String(), nil
return stringSliceToVariableValue(outputList), nil
},
}
}
@ -188,39 +222,32 @@ func interpolationFuncCoalesce() ast.Function {
// compat we do this.
func interpolationFuncConcat() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
ReturnType: ast.TypeString,
ArgTypes: []ast.Type{ast.TypeAny},
ReturnType: ast.TypeList,
Variadic: true,
VariadicType: ast.TypeString,
VariadicType: ast.TypeAny,
Callback: func(args []interface{}) (interface{}, error) {
var b bytes.Buffer
var finalList []string
var isDeprecated = true
var finalListElements []string
for _, arg := range args {
argument := arg.(string)
if len(argument) == 0 {
// Append strings for backward compatibility
if argument, ok := arg.(string); ok {
finalListElements = append(finalListElements, argument)
continue
}
if IsStringList(argument) {
isDeprecated = false
finalList = append(finalList, StringList(argument).Slice()...)
} else {
finalList = append(finalList, argument)
// Otherwise variables
if argument, ok := arg.([]ast.Variable); ok {
for _, element := range argument {
finalListElements = append(finalListElements, element.Value.(string))
}
continue
}
// Deprecated concat behaviour
b.WriteString(argument)
return nil, fmt.Errorf("arguments to concat() must be a string or list")
}
if isDeprecated {
return b.String(), nil
}
return NewStringList(finalList).String(), nil
return stringSliceToVariableValue(finalListElements), nil
},
}
}
@ -265,10 +292,10 @@ func interpolationFuncFormat() ast.Function {
// string formatting on lists.
func interpolationFuncFormatList() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
ArgTypes: []ast.Type{ast.TypeAny},
Variadic: true,
VariadicType: ast.TypeAny,
ReturnType: ast.TypeString,
ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) {
// Make a copy of the variadic part of args
// to avoid modifying the original.
@ -279,15 +306,15 @@ func interpolationFuncFormatList() ast.Function {
// Confirm along the way that all lists have the same length (n).
var n int
for i := 1; i < len(args); i++ {
s, ok := args[i].(string)
s, ok := args[i].([]ast.Variable)
if !ok {
continue
}
if !IsStringList(s) {
continue
}
parts := StringList(s).Slice()
parts, err := listVariableValueToStringSlice(s)
if err != nil {
return nil, err
}
// otherwise the list is sent down to be indexed
varargs[i-1] = parts
@ -324,7 +351,7 @@ func interpolationFuncFormatList() ast.Function {
}
list[i] = fmt.Sprintf(format, fmtargs...)
}
return NewStringList(list).String(), nil
return stringSliceToVariableValue(list), nil
},
}
}
@ -333,13 +360,13 @@ func interpolationFuncFormatList() ast.Function {
// find the index of a specific element in a list
func interpolationFuncIndex() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ArgTypes: []ast.Type{ast.TypeList, ast.TypeString},
ReturnType: ast.TypeInt,
Callback: func(args []interface{}) (interface{}, error) {
haystack := StringList(args[0].(string)).Slice()
haystack := args[0].([]ast.Variable)
needle := args[1].(string)
for index, element := range haystack {
if needle == element {
if needle == element.Value {
return index, nil
}
}
@ -352,13 +379,28 @@ func interpolationFuncIndex() ast.Function {
// multi-variable values to be joined by some character.
func interpolationFuncJoin() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ReturnType: ast.TypeString,
ArgTypes: []ast.Type{ast.TypeString},
Variadic: true,
VariadicType: ast.TypeList,
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
var list []string
if len(args) < 2 {
return nil, fmt.Errorf("not enough arguments to join()")
}
for _, arg := range args[1:] {
parts := StringList(arg.(string)).Slice()
list = append(list, parts...)
if parts, ok := arg.(ast.Variable); ok {
for _, part := range parts.Value.([]ast.Variable) {
list = append(list, part.Value.(string))
}
}
if parts, ok := arg.([]ast.Variable); ok {
for _, part := range parts {
list = append(list, part.Value.(string))
}
}
}
return strings.Join(list, args[0].(string)), nil
@ -412,19 +454,20 @@ func interpolationFuncReplace() ast.Function {
func interpolationFuncLength() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
ArgTypes: []ast.Type{ast.TypeAny},
ReturnType: ast.TypeInt,
Variadic: false,
Callback: func(args []interface{}) (interface{}, error) {
if !IsStringList(args[0].(string)) {
return len(args[0].(string)), nil
subject := args[0]
switch typedSubject := subject.(type) {
case string:
return len(typedSubject), nil
case []ast.Variable:
return len(typedSubject), nil
}
length := 0
for _, arg := range args {
length += StringList(arg.(string)).Length()
}
return length, nil
return 0, fmt.Errorf("arguments to length() must be a string or list")
},
}
}
@ -453,11 +496,12 @@ func interpolationFuncSignum() ast.Function {
func interpolationFuncSplit() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ReturnType: ast.TypeString,
ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) {
sep := args[0].(string)
s := args[1].(string)
return NewStringList(strings.Split(s, sep)).String(), nil
elements := strings.Split(s, sep)
return stringSliceToVariableValue(elements), nil
},
}
}
@ -466,20 +510,22 @@ func interpolationFuncSplit() ast.Function {
// dynamic lookups of map types within a Terraform configuration.
func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ArgTypes: []ast.Type{ast.TypeMap, ast.TypeString},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
k := fmt.Sprintf("var.%s.%s", args[0].(string), args[1].(string))
v, ok := vs[k]
index := args[1].(string)
mapVar := args[0].(map[string]ast.Variable)
v, ok := mapVar[index]
if !ok {
return "", fmt.Errorf(
"lookup in '%s' failed to find '%s'",
args[0].(string), args[1].(string))
"lookup failed to find '%s'",
args[1].(string))
}
if v.Type != ast.TypeString {
return "", fmt.Errorf(
"lookup in '%s' for '%s' has bad type %s",
args[0].(string), args[1].(string), v.Type)
"lookup for '%s' has bad type %s",
args[1].(string), v.Type)
}
return v.Value.(string), nil
@ -492,10 +538,10 @@ func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function {
// wrap if the index is larger than the number of elements in the multi-variable value.
func interpolationFuncElement() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ArgTypes: []ast.Type{ast.TypeList, ast.TypeString},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
list := StringList(args[0].(string))
list := args[0].([]ast.Variable)
index, err := strconv.Atoi(args[1].(string))
if err != nil || index < 0 {
@ -503,7 +549,9 @@ func interpolationFuncElement() ast.Function {
"invalid number for index, got %s", args[1])
}
v := list.Element(index)
resolvedIndex := index % len(list)
v := list[resolvedIndex].Value
return v, nil
},
}
@ -513,28 +561,20 @@ func interpolationFuncElement() ast.Function {
// keys of map types within a Terraform configuration.
func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
ReturnType: ast.TypeString,
ArgTypes: []ast.Type{ast.TypeMap},
ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) {
// Prefix must include ending dot to be a map
prefix := fmt.Sprintf("var.%s.", args[0].(string))
keys := make([]string, 0, len(vs))
for k, _ := range vs {
if !strings.HasPrefix(k, prefix) {
continue
}
keys = append(keys, k[len(prefix):])
}
mapVar := args[0].(map[string]ast.Variable)
keys := make([]string, 0)
if len(keys) <= 0 {
return "", fmt.Errorf(
"failed to find map '%s'",
args[0].(string))
for k, _ := range mapVar {
keys = append(keys, k)
}
sort.Strings(keys)
return NewStringList(keys).String(), nil
//Keys are guaranteed to be strings
return stringSliceToVariableValue(keys), nil
},
}
}
@ -543,38 +583,34 @@ func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
// keys of map types within a Terraform configuration.
func interpolationFuncValues(vs map[string]ast.Variable) ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
ReturnType: ast.TypeString,
ArgTypes: []ast.Type{ast.TypeMap},
ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) {
// Prefix must include ending dot to be a map
prefix := fmt.Sprintf("var.%s.", args[0].(string))
keys := make([]string, 0, len(vs))
for k, _ := range vs {
if !strings.HasPrefix(k, prefix) {
continue
}
keys = append(keys, k)
}
mapVar := args[0].(map[string]ast.Variable)
keys := make([]string, 0)
if len(keys) <= 0 {
return "", fmt.Errorf(
"failed to find map '%s'",
args[0].(string))
for k, _ := range mapVar {
keys = append(keys, k)
}
sort.Strings(keys)
vals := make([]string, 0, len(keys))
for _, k := range keys {
v := vs[k]
if v.Type != ast.TypeString {
return "", fmt.Errorf("values(): %q has bad type %s", k, v.Type)
values := make([]string, len(keys))
for index, key := range keys {
if value, ok := mapVar[key].Value.(string); ok {
values[index] = value
} else {
return "", fmt.Errorf("values(): %q has element with bad type %s",
key, mapVar[key].Type)
}
vals = append(vals, vs[k].Value.(string))
}
return NewStringList(vals).String(), nil
variable, err := hil.InterfaceToVariable(values)
if err != nil {
return nil, err
}
return variable.Value, nil
},
}
}

View File

@ -17,21 +17,21 @@ func TestInterpolateFuncCompact(t *testing.T) {
// empty string within array
{
`${compact(split(",", "a,,b"))}`,
NewStringList([]string{"a", "b"}).String(),
[]interface{}{"a", "b"},
false,
},
// empty string at the end of array
{
`${compact(split(",", "a,b,"))}`,
NewStringList([]string{"a", "b"}).String(),
[]interface{}{"a", "b"},
false,
},
// single empty string
{
`${compact(split(",", ""))}`,
NewStringList([]string{}).String(),
[]interface{}{},
false,
},
},
@ -174,76 +174,52 @@ func TestInterpolateFuncCoalesce(t *testing.T) {
})
}
func TestInterpolateFuncDeprecatedConcat(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{
{
`${concat("foo", "bar")}`,
"foobar",
false,
},
{
`${concat("foo")}`,
"foo",
false,
},
{
`${concat()}`,
nil,
true,
},
},
})
}
func TestInterpolateFuncConcat(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{
// String + list
{
`${concat("a", split(",", "b,c"))}`,
NewStringList([]string{"a", "b", "c"}).String(),
[]interface{}{"a", "b", "c"},
false,
},
// List + string
{
`${concat(split(",", "a,b"), "c")}`,
NewStringList([]string{"a", "b", "c"}).String(),
[]interface{}{"a", "b", "c"},
false,
},
// Single list
{
`${concat(split(",", ",foo,"))}`,
NewStringList([]string{"", "foo", ""}).String(),
[]interface{}{"", "foo", ""},
false,
},
{
`${concat(split(",", "a,b,c"))}`,
NewStringList([]string{"a", "b", "c"}).String(),
[]interface{}{"a", "b", "c"},
false,
},
// Two lists
{
`${concat(split(",", "a,b,c"), split(",", "d,e"))}`,
NewStringList([]string{"a", "b", "c", "d", "e"}).String(),
[]interface{}{"a", "b", "c", "d", "e"},
false,
},
// Two lists with different separators
{
`${concat(split(",", "a,b,c"), split(" ", "d e"))}`,
NewStringList([]string{"a", "b", "c", "d", "e"}).String(),
[]interface{}{"a", "b", "c", "d", "e"},
false,
},
// More lists
{
`${concat(split(",", "a,b"), split(",", "c,d"), split(",", "e,f"), split(",", "0,1"))}`,
NewStringList([]string{"a", "b", "c", "d", "e", "f", "0", "1"}).String(),
[]interface{}{"a", "b", "c", "d", "e", "f", "0", "1"},
false,
},
},
@ -338,7 +314,7 @@ func TestInterpolateFuncFormatList(t *testing.T) {
// formatlist applies to each list element in turn
{
`${formatlist("<%s>", split(",", "A,B"))}`,
NewStringList([]string{"<A>", "<B>"}).String(),
[]interface{}{"<A>", "<B>"},
false,
},
// formatlist repeats scalar elements
@ -362,7 +338,7 @@ func TestInterpolateFuncFormatList(t *testing.T) {
// Works with lists of length 1 [GH-2240]
{
`${formatlist("%s.id", split(",", "demo-rest-elb"))}`,
NewStringList([]string{"demo-rest-elb.id"}).String(),
[]interface{}{"demo-rest-elb.id"},
false,
},
},
@ -371,6 +347,11 @@ func TestInterpolateFuncFormatList(t *testing.T) {
func TestInterpolateFuncIndex(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.list1": interfaceToVariableSwallowError([]string{"notfoo", "stillnotfoo", "bar"}),
"var.list2": interfaceToVariableSwallowError([]string{"foo"}),
"var.list3": interfaceToVariableSwallowError([]string{"foo", "spam", "bar", "eggs"}),
},
Cases: []testFunctionCase{
{
`${index("test", "")}`,
@ -379,22 +360,19 @@ func TestInterpolateFuncIndex(t *testing.T) {
},
{
fmt.Sprintf(`${index("%s", "foo")}`,
NewStringList([]string{"notfoo", "stillnotfoo", "bar"}).String()),
`${index(var.list1, "foo")}`,
nil,
true,
},
{
fmt.Sprintf(`${index("%s", "foo")}`,
NewStringList([]string{"foo"}).String()),
`${index(var.list2, "foo")}`,
"0",
false,
},
{
fmt.Sprintf(`${index("%s", "bar")}`,
NewStringList([]string{"foo", "spam", "bar", "eggs"}).String()),
`${index(var.list3, "bar")}`,
"2",
false,
},
@ -404,6 +382,10 @@ func TestInterpolateFuncIndex(t *testing.T) {
func TestInterpolateFuncJoin(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.a_list": interfaceToVariableSwallowError([]string{"foo"}),
"var.a_longer_list": interfaceToVariableSwallowError([]string{"foo", "bar", "baz"}),
},
Cases: []testFunctionCase{
{
`${join(",")}`,
@ -412,24 +394,13 @@ func TestInterpolateFuncJoin(t *testing.T) {
},
{
fmt.Sprintf(`${join(",", "%s")}`,
NewStringList([]string{"foo"}).String()),
`${join(",", var.a_list)}`,
"foo",
false,
},
/*
TODO
{
`${join(",", "foo", "bar")}`,
"foo,bar",
false,
},
*/
{
fmt.Sprintf(`${join(".", "%s")}`,
NewStringList([]string{"foo", "bar", "baz"}).String()),
`${join(".", var.a_longer_list)}`,
"foo.bar.baz",
false,
},
@ -632,37 +603,37 @@ func TestInterpolateFuncSplit(t *testing.T) {
{
`${split(",", "")}`,
NewStringList([]string{""}).String(),
[]interface{}{""},
false,
},
{
`${split(",", "foo")}`,
NewStringList([]string{"foo"}).String(),
[]interface{}{"foo"},
false,
},
{
`${split(",", ",,,")}`,
NewStringList([]string{"", "", "", ""}).String(),
[]interface{}{"", "", "", ""},
false,
},
{
`${split(",", "foo,")}`,
NewStringList([]string{"foo", ""}).String(),
[]interface{}{"foo", ""},
false,
},
{
`${split(",", ",foo,")}`,
NewStringList([]string{"", "foo", ""}).String(),
[]interface{}{"", "foo", ""},
false,
},
{
`${split(".", "foo.bar.baz")}`,
NewStringList([]string{"foo", "bar", "baz"}).String(),
[]interface{}{"foo", "bar", "baz"},
false,
},
},
@ -672,28 +643,33 @@ func TestInterpolateFuncSplit(t *testing.T) {
func TestInterpolateFuncLookup(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.foo.bar": ast.Variable{
Value: "baz",
Type: ast.TypeString,
"var.foo": ast.Variable{
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"bar": ast.Variable{
Type: ast.TypeString,
Value: "baz",
},
},
},
},
Cases: []testFunctionCase{
{
`${lookup("foo", "bar")}`,
`${lookup(var.foo, "bar")}`,
"baz",
false,
},
// Invalid key
{
`${lookup("foo", "baz")}`,
`${lookup(var.foo, "baz")}`,
nil,
true,
},
// Too many args
{
`${lookup("foo", "bar", "baz")}`,
`${lookup(var.foo, "bar", "baz")}`,
nil,
true,
},
@ -704,13 +680,18 @@ func TestInterpolateFuncLookup(t *testing.T) {
func TestInterpolateFuncKeys(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.foo.bar": ast.Variable{
Value: "baz",
Type: ast.TypeString,
},
"var.foo.qux": ast.Variable{
Value: "quack",
Type: ast.TypeString,
"var.foo": ast.Variable{
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"bar": ast.Variable{
Value: "baz",
Type: ast.TypeString,
},
"qux": ast.Variable{
Value: "quack",
Type: ast.TypeString,
},
},
},
"var.str": ast.Variable{
Value: "astring",
@ -719,28 +700,28 @@ func TestInterpolateFuncKeys(t *testing.T) {
},
Cases: []testFunctionCase{
{
`${keys("foo")}`,
NewStringList([]string{"bar", "qux"}).String(),
`${keys(var.foo)}`,
[]interface{}{"bar", "qux"},
false,
},
// Invalid key
{
`${keys("not")}`,
`${keys(var.not)}`,
nil,
true,
},
// Too many args
{
`${keys("foo", "bar")}`,
`${keys(var.foo, "bar")}`,
nil,
true,
},
// Not a map
{
`${keys("str")}`,
`${keys(var.str)}`,
nil,
true,
},
@ -751,13 +732,18 @@ func TestInterpolateFuncKeys(t *testing.T) {
func TestInterpolateFuncValues(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.foo.bar": ast.Variable{
Value: "quack",
Type: ast.TypeString,
},
"var.foo.qux": ast.Variable{
Value: "baz",
Type: ast.TypeString,
"var.foo": ast.Variable{
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"bar": ast.Variable{
Value: "quack",
Type: ast.TypeString,
},
"qux": ast.Variable{
Value: "baz",
Type: ast.TypeString,
},
},
},
"var.str": ast.Variable{
Value: "astring",
@ -766,28 +752,28 @@ func TestInterpolateFuncValues(t *testing.T) {
},
Cases: []testFunctionCase{
{
`${values("foo")}`,
NewStringList([]string{"quack", "baz"}).String(),
`${values(var.foo)}`,
[]interface{}{"quack", "baz"},
false,
},
// Invalid key
{
`${values("not")}`,
`${values(var.not)}`,
nil,
true,
},
// Too many args
{
`${values("foo", "bar")}`,
`${values(var.foo, "bar")}`,
nil,
true,
},
// Not a map
{
`${values("str")}`,
`${values(var.str)}`,
nil,
true,
},
@ -795,43 +781,47 @@ func TestInterpolateFuncValues(t *testing.T) {
})
}
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
func TestInterpolateFuncElement(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.a_list": interfaceToVariableSwallowError([]string{"foo", "baz"}),
"var.a_short_list": interfaceToVariableSwallowError([]string{"foo"}),
},
Cases: []testFunctionCase{
{
fmt.Sprintf(`${element("%s", "1")}`,
NewStringList([]string{"foo", "baz"}).String()),
`${element(var.a_list, "1")}`,
"baz",
false,
},
{
fmt.Sprintf(`${element("%s", "0")}`,
NewStringList([]string{"foo"}).String()),
`${element(var.a_short_list, "0")}`,
"foo",
false,
},
// Invalid index should wrap vs. out-of-bounds
{
fmt.Sprintf(`${element("%s", "2")}`,
NewStringList([]string{"foo", "baz"}).String()),
`${element(var.a_list, "2")}`,
"foo",
false,
},
// Negative number should fail
{
fmt.Sprintf(`${element("%s", "-1")}`,
NewStringList([]string{"foo"}).String()),
`${element(var.a_short_list, "-1")}`,
nil,
true,
},
// Too many args
{
fmt.Sprintf(`${element("%s", "0", "2")}`,
NewStringList([]string{"foo", "baz"}).String()),
`${element(var.a_list, "0", "2")}`,
nil,
true,
},

View File

@ -42,7 +42,7 @@ type interpolationWalker struct {
//
// If Replace is set to false in interpolationWalker, then the replace
// value can be anything as it will have no effect.
type interpolationWalkerFunc func(ast.Node) (string, error)
type interpolationWalkerFunc func(ast.Node) (interface{}, error)
// interpolationWalkerContextFunc is called by interpolationWalk if
// ContextF is set. This receives both the interpolation and the location
@ -150,12 +150,15 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
// set if it is computed. This behavior is different if we're
// splitting (in a SliceElem) or not.
remove := false
if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal) {
parts := StringList(replaceVal).Slice()
for _, p := range parts {
if p == UnknownVariableValue {
if w.loc == reflectwalk.SliceElem {
switch typedReplaceVal := replaceVal.(type) {
case string:
if typedReplaceVal == UnknownVariableValue {
remove = true
}
case []interface{}:
if hasUnknownValue(typedReplaceVal) {
remove = true
break
}
}
} else if replaceVal == UnknownVariableValue {
@ -226,63 +229,63 @@ func (w *interpolationWalker) replaceCurrent(v reflect.Value) {
}
}
func hasUnknownValue(variable []interface{}) bool {
for _, value := range variable {
if strVal, ok := value.(string); ok {
if strVal == UnknownVariableValue {
return true
}
}
}
return false
}
func (w *interpolationWalker) splitSlice() {
// Get the []interface{} slice so we can do some operations on
// it without dealing with reflection. We'll document each step
// here to be clear.
var s []interface{}
raw := w.cs[len(w.cs)-1]
var s []interface{}
switch v := raw.Interface().(type) {
case []interface{}:
s = v
case []map[string]interface{}:
return
default:
panic("Unknown kind: " + raw.Kind().String())
}
// Check if we have any elements that we need to split. If not, then
// just return since we're done.
split := false
for _, v := range s {
sv, ok := v.(string)
if !ok {
continue
}
if IsStringList(sv) {
for _, val := range s {
if varVal, ok := val.(ast.Variable); ok && varVal.Type == ast.TypeList {
split = true
}
if _, ok := val.([]interface{}); ok {
split = true
break
}
}
if !split {
return
}
// Make a new result slice that is twice the capacity to fit our growth.
result := make([]interface{}, 0, len(s)*2)
// Go over each element of the original slice and start building up
// the resulting slice by splitting where we have to.
result := make([]interface{}, 0)
for _, v := range s {
sv, ok := v.(string)
if !ok {
// Not a string, so just set it
result = append(result, v)
continue
}
if IsStringList(sv) {
for _, p := range StringList(sv).Slice() {
result = append(result, p)
switch val := v.(type) {
case ast.Variable:
switch val.Type {
case ast.TypeList:
elements := val.Value.([]ast.Variable)
for _, element := range elements {
result = append(result, element.Value)
}
default:
result = append(result, val.Value)
}
continue
case []interface{}:
for _, element := range val {
result = append(result, element)
}
default:
result = append(result, v)
}
// Not a string list, so just set it
result = append(result, sv)
}
// Our slice is now done, we have to replace the slice now
// with this new one that we have.
w.replaceCurrent(reflect.ValueOf(result))
}

View File

@ -89,7 +89,7 @@ func TestInterpolationWalker_detect(t *testing.T) {
for i, tc := range cases {
var actual []string
detectFn := func(root ast.Node) (string, error) {
detectFn := func(root ast.Node) (interface{}, error) {
actual = append(actual, fmt.Sprintf("%s", root))
return "", nil
}
@ -109,7 +109,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
cases := []struct {
Input interface{}
Output interface{}
Value string
Value interface{}
}{
{
Input: map[string]interface{}{
@ -159,7 +159,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
"bing",
},
},
Value: NewStringList([]string{"bar", "baz"}).String(),
Value: []interface{}{"bar", "baz"},
},
{
@ -170,12 +170,12 @@ func TestInterpolationWalker_replace(t *testing.T) {
},
},
Output: map[string]interface{}{},
Value: NewStringList([]string{UnknownVariableValue, "baz"}).String(),
Value: []interface{}{UnknownVariableValue, "baz"},
},
}
for i, tc := range cases {
fn := func(ast.Node) (string, error) {
fn := func(ast.Node) (interface{}, error) {
return tc.Value, nil
}

View File

@ -108,7 +108,7 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
defer r.lock.Unlock()
config := langEvalConfig(vs)
return r.interpolate(func(root ast.Node) (string, error) {
return r.interpolate(func(root ast.Node) (interface{}, error) {
// We detect the variables again and check if the value of any
// of the variables is the computed value. If it is, then we
// treat this entire value as computed.
@ -137,7 +137,7 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
return "", err
}
return result.Value.(string), nil
return result.Value, nil
})
}
@ -194,7 +194,7 @@ func (r *RawConfig) init() error {
r.Interpolations = nil
r.Variables = nil
fn := func(node ast.Node) (string, error) {
fn := func(node ast.Node) (interface{}, error) {
r.Interpolations = append(r.Interpolations, node)
vars, err := DetectVariables(node)
if err != nil {

View File

@ -1,89 +0,0 @@
package config
import (
"fmt"
"strings"
)
// StringList represents the "poor man's list" that terraform uses
// internally
type StringList string
// This is the delimiter used to recognize and split StringLists
//
// It plays two semantic roles:
// * It introduces a list
// * It terminates each element
//
// Example representations:
// [] => SLD
// [""] => SLDSLD
// [" "] => SLD SLD
// ["foo"] => SLDfooSLD
// ["foo", "bar"] => SLDfooSLDbarSLD
// ["", ""] => SLDSLDSLD
const stringListDelim = `B780FFEC-B661-4EB8-9236-A01737AD98B6`
// Takes a Stringlist and returns one without empty strings in it
func (sl StringList) Compact() StringList {
parts := sl.Slice()
newlist := []string{}
// drop the empty strings
for i := range parts {
if parts[i] != "" {
newlist = append(newlist, parts[i])
}
}
return NewStringList(newlist)
}
// Build a StringList from a slice
func NewStringList(parts []string) StringList {
// We have to special case the empty list representation
if len(parts) == 0 {
return StringList(stringListDelim)
}
return StringList(fmt.Sprintf("%s%s%s",
stringListDelim,
strings.Join(parts, stringListDelim),
stringListDelim,
))
}
// Returns an element at the index, wrapping around the length of the string
// when index > list length
func (sl StringList) Element(index int) string {
if sl.Length() == 0 {
return ""
}
return sl.Slice()[index%sl.Length()]
}
// Returns the length of the StringList
func (sl StringList) Length() int {
return len(sl.Slice())
}
// Returns a slice of strings as represented by this StringList
func (sl StringList) Slice() []string {
parts := strings.Split(string(sl), stringListDelim)
// split on an empty StringList will have a length of 2, since there is
// always at least one deliminator
if len(parts) <= 2 {
return []string{}
}
// strip empty elements generated by leading and trailing delimiters
return parts[1 : len(parts)-1]
}
func (sl StringList) String() string {
return string(sl)
}
// Determines if a given string represents a StringList
func IsStringList(s string) bool {
return strings.Contains(s, stringListDelim)
}

View File

@ -1,52 +0,0 @@
package config
import (
"reflect"
"testing"
)
func TestStringList_slice(t *testing.T) {
expected := []string{"apple", "banana", "pear"}
l := NewStringList(expected)
actual := l.Slice()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("Expected %q, got %q", expected, actual)
}
}
func TestStringList_element(t *testing.T) {
list := []string{"apple", "banana", "pear"}
l := NewStringList(list)
actual := l.Element(1)
expected := "banana"
if actual != expected {
t.Fatalf("Expected 2nd element from %q to be %q, got %q",
list, expected, actual)
}
}
func TestStringList_empty_slice(t *testing.T) {
expected := []string{}
l := NewStringList(expected)
actual := l.Slice()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("Expected %q, got %q", expected, actual)
}
}
func TestStringList_empty_slice_length(t *testing.T) {
list := []string{}
l := NewStringList([]string{})
actual := l.Length()
expected := 0
if actual != expected {
t.Fatalf("Expected length of %q to be %d, got %d",
list, expected, actual)
}
}

View File

@ -0,0 +1,4 @@
module "foo" {
source = "./foo"
nodes = [1,2,3]
}

View File

@ -1,4 +1,7 @@
module "foo" {
source = "./foo"
nodes = [1,2,3]
nodes = {
key1 = "value1"
key2 = "value2"
}
}

84
help.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"bytes"
"fmt"
"log"
"sort"
"strings"
"github.com/mitchellh/cli"
)
// helpFunc is a cli.HelpFunc that can is used to output the help for Terraform.
func helpFunc(commands map[string]cli.CommandFactory) string {
// Determine the maximum key length, and classify based on type
porcelain := make(map[string]cli.CommandFactory)
plumbing := make(map[string]cli.CommandFactory)
maxKeyLen := 0
for key, f := range commands {
if len(key) > maxKeyLen {
maxKeyLen = len(key)
}
if _, ok := PlumbingCommands[key]; ok {
plumbing[key] = f
} else {
porcelain[key] = f
}
}
var buf bytes.Buffer
buf.WriteString("usage: terraform [--version] [--help] <command> [args]\n\n")
buf.WriteString(
"The available commands for execution are listed below.\n" +
"The most common, useful commands are shown first, followed by\n" +
"less common or more advanced commands. If you're just getting\n" +
"started with Terraform, stick with the common commands. For the\n" +
"other commands, please read the help and docs before usage.\n\n")
buf.WriteString("Common commands:\n")
buf.WriteString(listCommands(porcelain, maxKeyLen))
buf.WriteString("\nAll other commands:\n")
buf.WriteString(listCommands(plumbing, maxKeyLen))
return buf.String()
}
// listCommands just lists the commands in the map with the
// given maximum key length.
func listCommands(commands map[string]cli.CommandFactory, maxKeyLen int) string {
var buf bytes.Buffer
// Get the list of keys so we can sort them, and also get the maximum
// key length so they can be aligned properly.
keys := make([]string, 0, len(commands))
for key, _ := range commands {
// This is an internal command that users should never call directly so
// we will hide it from the command listing.
if key == "internal-plugin" {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
commandFunc, ok := commands[key]
if !ok {
// This should never happen since we JUST built the list of
// keys.
panic("command not found: " + key)
}
command, err := commandFunc()
if err != nil {
log.Printf("[ERR] cli: Command '%s' failed to load: %s",
key, err)
continue
}
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis()))
}
return buf.String()
}

View File

@ -284,7 +284,10 @@ func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r
// Initialize the context
opts.Module = mod
opts.State = state
ctx := terraform.NewContext(&opts)
ctx, err := terraform.NewContext(&opts)
if err != nil {
return err
}
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
if len(es) > 0 {
estrs := make([]string, len(es))
@ -362,7 +365,10 @@ func testStep(
opts.Module = mod
opts.State = state
opts.Destroy = step.Destroy
ctx := terraform.NewContext(&opts)
ctx, err := terraform.NewContext(&opts)
if err != nil {
return state, fmt.Errorf("Error initializing context: %s", err)
}
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
if len(es) > 0 {
estrs := make([]string, len(es))

View File

@ -100,7 +100,8 @@ func (r *ConfigFieldReader) readField(
func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
// We want both the raw value and the interpolated. We use the interpolated
// to store actual values and we use the raw one to check for
// computed keys.
// computed keys. Actual values are obtained in the switch, depending on
// the type of the raw value.
mraw, ok := r.Config.GetRaw(k)
if !ok {
return FieldReadResult{}, nil
@ -109,6 +110,25 @@ func (r *ConfigFieldReader) readMap(k string) (FieldReadResult, error) {
result := make(map[string]interface{})
computed := false
switch m := mraw.(type) {
case string:
// This is a map which has come out of an interpolated variable, so we
// can just get the value directly from config. Values cannot be computed
// currently.
v, _ := r.Config.Get(k)
// If this isn't a map[string]interface, it must be computed.
mapV, ok := v.(map[string]interface{})
if !ok {
return FieldReadResult{
Exists: true,
Computed: true,
}, nil
}
// Otherwise we can proceed as usual.
for i, iv := range mapV {
result[i] = iv
}
case []interface{}:
for i, innerRaw := range m {
for ik := range innerRaw.(map[string]interface{}) {

View File

@ -183,6 +183,36 @@ func TestConfigFieldReader_ComputedMap(t *testing.T) {
}),
false,
},
"native map": {
[]string{"map"},
FieldReadResult{
Value: map[string]interface{}{
"bar": "baz",
"baz": "bar",
},
Exists: true,
Computed: false,
},
testConfigInterpolate(t, map[string]interface{}{
"map": "${var.foo}",
}, map[string]ast.Variable{
"var.foo": ast.Variable{
Type: ast.TypeMap,
Value: map[string]ast.Variable{
"bar": ast.Variable{
Type: ast.TypeString,
Value: "baz",
},
"baz": ast.Variable{
Type: ast.TypeString,
Value: "bar",
},
},
},
}),
false,
},
}
for name, tc := range cases {
@ -305,6 +335,7 @@ func testConfigInterpolate(
t *testing.T,
raw map[string]interface{},
vs map[string]ast.Variable) *terraform.ResourceConfig {
rc, err := config.NewRawConfig(raw)
if err != nil {
t.Fatalf("err: %s", err)

View File

@ -19,8 +19,10 @@ import (
"strconv"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
"log"
)
// Schema is used to describe the structure of a value.
@ -1120,11 +1122,21 @@ func (m schemaMap) validateMap(
// case to []interface{} unless the slice is exactly that type.
rawV := reflect.ValueOf(raw)
switch rawV.Kind() {
case reflect.String:
// If raw and reified are equal, this is a string and should
// be rejected.
reified, reifiedOk := c.Get(k)
log.Printf("[jen20] reified: %s", spew.Sdump(reified))
log.Printf("[jen20] raw: %s", spew.Sdump(raw))
if reifiedOk && raw == reified && !c.IsComputed(k) {
return nil, []error{fmt.Errorf("%s: should be a map", k)}
}
// Otherwise it's likely raw is an interpolation.
return nil, nil
case reflect.Map:
case reflect.Slice:
default:
return nil, []error{fmt.Errorf(
"%s: should be a map", k)}
return nil, []error{fmt.Errorf("%s: should be a map", k)}
}
// If it is not a slice, it is valid

View File

@ -8,6 +8,7 @@ import (
"strconv"
"testing"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/hashcode"
@ -123,12 +124,17 @@ func TestValueType_Zero(t *testing.T) {
}
}
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
func TestSchemaMap_Diff(t *testing.T) {
cases := map[string]struct {
Schema map[string]*Schema
State *terraform.InstanceState
Config map[string]interface{}
ConfigVariables map[string]string
ConfigVariables map[string]ast.Variable
Diff *terraform.InstanceDiff
Err bool
}{
@ -396,8 +402,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"availability_zone": "${var.foo}",
},
ConfigVariables: map[string]string{
"var.foo": "bar",
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError("bar"),
},
Diff: &terraform.InstanceDiff{
@ -426,8 +432,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"availability_zone": "${var.foo}",
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue,
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
@ -576,8 +582,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}"},
},
ConfigVariables: map[string]string{
"var.foo": config.NewStringList([]string{"2", "5"}).String(),
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
},
Diff: &terraform.InstanceDiff{
@ -619,9 +625,9 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}"},
},
ConfigVariables: map[string]string{
"var.foo": config.NewStringList([]string{
config.UnknownVariableValue, "5"}).String(),
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{
config.UnknownVariableValue, "5"}),
},
Diff: &terraform.InstanceDiff{
@ -886,8 +892,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{"${var.foo}", 1},
},
ConfigVariables: map[string]string{
"var.foo": config.NewStringList([]string{"2", "5"}).String(),
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
},
Diff: &terraform.InstanceDiff{
@ -932,9 +938,9 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}"},
},
ConfigVariables: map[string]string{
"var.foo": config.NewStringList([]string{
config.UnknownVariableValue, "5"}).String(),
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{
config.UnknownVariableValue, "5"}),
},
Diff: &terraform.InstanceDiff{
@ -1603,8 +1609,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"instances": []interface{}{"${var.foo}"},
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue,
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
@ -1654,8 +1660,8 @@ func TestSchemaMap_Diff(t *testing.T) {
},
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue,
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
@ -1720,8 +1726,8 @@ func TestSchemaMap_Diff(t *testing.T) {
},
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue,
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
@ -1787,8 +1793,8 @@ func TestSchemaMap_Diff(t *testing.T) {
},
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue,
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
@ -2134,8 +2140,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}32"},
},
ConfigVariables: map[string]string{
"var.foo": config.UnknownVariableValue,
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
@ -2403,12 +2409,7 @@ func TestSchemaMap_Diff(t *testing.T) {
}
if len(tc.ConfigVariables) > 0 {
vars := make(map[string]ast.Variable)
for k, v := range tc.ConfigVariables {
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
}
if err := c.Interpolate(vars); err != nil {
if err := c.Interpolate(tc.ConfigVariables); err != nil {
t.Fatalf("#%q err: %s", tn, err)
}
}
@ -2420,7 +2421,7 @@ func TestSchemaMap_Diff(t *testing.T) {
}
if !reflect.DeepEqual(tc.Diff, d) {
t.Fatalf("#%q:\n\nexpected: %#v\n\ngot:\n\n%#v", tn, tc.Diff, d)
t.Fatalf("#%q:\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Diff, d)
}
}
}

View File

@ -9,8 +9,8 @@ import (
"runtime"
"sync"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/plugin"
"github.com/mattn/go-colorable"
"github.com/mitchellh/cli"
"github.com/mitchellh/panicwrap"
@ -18,6 +18,8 @@ import (
)
func main() {
// Override global prefix set by go-dynect during init()
log.SetPrefix("")
os.Exit(realMain())
}
@ -86,7 +88,7 @@ func wrappedMain() int {
// Load the configuration
config := BuiltinConfig
if err := config.Discover(); err != nil {
if err := config.Discover(Ui); err != nil {
Ui.Error(fmt.Sprintf("Error discovering plugins: %s", err))
return 1
}
@ -113,7 +115,7 @@ func wrappedMain() int {
cli := &cli.CLI{
Args: args,
Commands: Commands,
HelpFunc: cli.BasicHelpFunc("terraform"),
HelpFunc: helpFunc,
HelpWriter: os.Stdout,
}

View File

@ -1,339 +0,0 @@
package plugin
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"unicode"
tfrpc "github.com/hashicorp/terraform/rpc"
)
// If this is true, then the "unexpected EOF" panic will not be
// raised throughout the clients.
var Killed = false
// This is a slice of the "managed" clients which are cleaned up when
// calling Cleanup
var managedClients = make([]*Client, 0, 5)
// Client handles the lifecycle of a plugin application, determining its
// RPC address, and returning various types of Terraform interface implementations
// across the multi-process communication layer.
type Client struct {
config *ClientConfig
exited bool
doneLogging chan struct{}
l sync.Mutex
address net.Addr
client *tfrpc.Client
}
// ClientConfig is the configuration used to initialize a new
// plugin client. After being used to initialize a plugin client,
// that configuration must not be modified again.
type ClientConfig struct {
// The unstarted subprocess for starting the plugin.
Cmd *exec.Cmd
// Managed represents if the client should be managed by the
// plugin package or not. If true, then by calling CleanupClients,
// it will automatically be cleaned up. Otherwise, the client
// user is fully responsible for making sure to Kill all plugin
// clients. By default the client is _not_ managed.
Managed bool
// The minimum and maximum port to use for communicating with
// the subprocess. If not set, this defaults to 10,000 and 25,000
// respectively.
MinPort, MaxPort uint
// StartTimeout is the timeout to wait for the plugin to say it
// has started successfully.
StartTimeout time.Duration
// If non-nil, then the stderr of the client will be written to here
// (as well as the log).
Stderr io.Writer
}
// This makes sure all the managed subprocesses are killed and properly
// logged. This should be called before the parent process running the
// plugins exits.
//
// This must only be called _once_.
func CleanupClients() {
// Set the killed to true so that we don't get unexpected panics
Killed = true
// Kill all the managed clients in parallel and use a WaitGroup
// to wait for them all to finish up.
var wg sync.WaitGroup
for _, client := range managedClients {
wg.Add(1)
go func(client *Client) {
client.Kill()
wg.Done()
}(client)
}
log.Println("[DEBUG] waiting for all plugin processes to complete...")
wg.Wait()
}
// Creates a new plugin client which manages the lifecycle of an external
// plugin and gets the address for the RPC connection.
//
// The client must be cleaned up at some point by calling Kill(). If
// the client is a managed client (created with NewManagedClient) you
// can just call CleanupClients at the end of your program and they will
// be properly cleaned.
func NewClient(config *ClientConfig) (c *Client) {
if config.MinPort == 0 && config.MaxPort == 0 {
config.MinPort = 10000
config.MaxPort = 25000
}
if config.StartTimeout == 0 {
config.StartTimeout = 1 * time.Minute
}
if config.Stderr == nil {
config.Stderr = ioutil.Discard
}
c = &Client{config: config}
if config.Managed {
managedClients = append(managedClients, c)
}
return
}
// Client returns an RPC client for the plugin.
//
// Subsequent calls to this will return the same RPC client.
func (c *Client) Client() (*tfrpc.Client, error) {
addr, err := c.Start()
if err != nil {
return nil, err
}
c.l.Lock()
defer c.l.Unlock()
if c.client != nil {
return c.client, nil
}
c.client, err = tfrpc.Dial(addr.Network(), addr.String())
if err != nil {
return nil, err
}
return c.client, nil
}
// Tells whether or not the underlying process has exited.
func (c *Client) Exited() bool {
c.l.Lock()
defer c.l.Unlock()
return c.exited
}
// End the executing subprocess (if it is running) and perform any cleanup
// tasks necessary such as capturing any remaining logs and so on.
//
// This method blocks until the process successfully exits.
//
// This method can safely be called multiple times.
func (c *Client) Kill() {
cmd := c.config.Cmd
if cmd.Process == nil {
return
}
cmd.Process.Kill()
// Wait for the client to finish logging so we have a complete log
<-c.doneLogging
}
// Starts the underlying subprocess, communicating with it to negotiate
// a port for RPC connections, and returning the address to connect via RPC.
//
// This method is safe to call multiple times. Subsequent calls have no effect.
// Once a client has been started once, it cannot be started again, even if
// it was killed.
func (c *Client) Start() (addr net.Addr, err error) {
c.l.Lock()
defer c.l.Unlock()
if c.address != nil {
return c.address, nil
}
c.doneLogging = make(chan struct{})
env := []string{
fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue),
fmt.Sprintf("TF_PLUGIN_MIN_PORT=%d", c.config.MinPort),
fmt.Sprintf("TF_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
}
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
cmd := c.config.Cmd
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, env...)
cmd.Stdin = os.Stdin
cmd.Stderr = stderr_w
cmd.Stdout = stdout_w
log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args)
err = cmd.Start()
if err != nil {
return
}
// Make sure the command is properly cleaned up if there is an error
defer func() {
r := recover()
if err != nil || r != nil {
cmd.Process.Kill()
}
if r != nil {
panic(r)
}
}()
// Start goroutine to wait for process to exit
exitCh := make(chan struct{})
go func() {
// Make sure we close the write end of our stderr/stdout so
// that the readers send EOF properly.
defer stderr_w.Close()
defer stdout_w.Close()
// Wait for the command to end.
cmd.Wait()
// Log and make sure to flush the logs write away
log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path)
os.Stderr.Sync()
// Mark that we exited
close(exitCh)
// Set that we exited, which takes a lock
c.l.Lock()
defer c.l.Unlock()
c.exited = true
}()
// Start goroutine that logs the stderr
go c.logStderr(stderr_r)
// Start a goroutine that is going to be reading the lines
// out of stdout
linesCh := make(chan []byte)
go func() {
defer close(linesCh)
buf := bufio.NewReader(stdout_r)
for {
line, err := buf.ReadBytes('\n')
if line != nil {
linesCh <- line
}
if err == io.EOF {
return
}
}
}()
// Make sure after we exit we read the lines from stdout forever
// so they don't block since it is an io.Pipe
defer func() {
go func() {
for _ = range linesCh {
}
}()
}()
// Some channels for the next step
timeout := time.After(c.config.StartTimeout)
// Start looking for the address
log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path)
select {
case <-timeout:
err = errors.New("timeout while waiting for plugin to start")
case <-exitCh:
err = errors.New("plugin exited before we could connect")
case lineBytes := <-linesCh:
// Trim the line and split by "|" in order to get the parts of
// the output.
line := strings.TrimSpace(string(lineBytes))
parts := strings.SplitN(line, "|", 3)
if len(parts) < 3 {
err = fmt.Errorf("Unrecognized remote plugin message: %s", line)
return
}
// Test the API version
if parts[0] != APIVersion {
err = fmt.Errorf("Incompatible API version with plugin. "+
"Plugin version: %s, Ours: %s", parts[0], APIVersion)
return
}
switch parts[1] {
case "tcp":
addr, err = net.ResolveTCPAddr("tcp", parts[2])
case "unix":
addr, err = net.ResolveUnixAddr("unix", parts[2])
default:
err = fmt.Errorf("Unknown address type: %s", parts[1])
}
}
c.address = addr
return
}
func (c *Client) logStderr(r io.Reader) {
bufR := bufio.NewReader(r)
for {
line, err := bufR.ReadString('\n')
if line != "" {
c.config.Stderr.Write([]byte(line))
line = strings.TrimRightFunc(line, unicode.IsSpace)
log.Printf("[DEBUG] %s: %s", filepath.Base(c.config.Cmd.Path), line)
}
if err == io.EOF {
break
}
}
// Flag that we've completed logging for others
close(c.doneLogging)
}

View File

@ -1,145 +0,0 @@
package plugin
import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"time"
)
func TestClient(t *testing.T) {
process := helperProcess("mock")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
// Test that it parses the proper address
addr, err := c.Start()
if err != nil {
t.Fatalf("err should be nil, got %s", err)
}
if addr.Network() != "tcp" {
t.Fatalf("bad: %#v", addr)
}
if addr.String() != ":1234" {
t.Fatalf("bad: %#v", addr)
}
// Test that it exits properly if killed
c.Kill()
if process.ProcessState == nil {
t.Fatal("should have process state")
}
// Test that it knows it is exited
if !c.Exited() {
t.Fatal("should say client has exited")
}
}
func TestClientStart_badVersion(t *testing.T) {
config := &ClientConfig{
Cmd: helperProcess("bad-version"),
StartTimeout: 50 * time.Millisecond,
}
c := NewClient(config)
defer c.Kill()
_, err := c.Start()
if err == nil {
t.Fatal("err should not be nil")
}
}
func TestClient_Start_Timeout(t *testing.T) {
config := &ClientConfig{
Cmd: helperProcess("start-timeout"),
StartTimeout: 50 * time.Millisecond,
}
c := NewClient(config)
defer c.Kill()
_, err := c.Start()
if err == nil {
t.Fatal("err should not be nil")
}
}
func TestClient_Stderr(t *testing.T) {
stderr := new(bytes.Buffer)
process := helperProcess("stderr")
c := NewClient(&ClientConfig{
Cmd: process,
Stderr: stderr,
})
defer c.Kill()
if _, err := c.Start(); err != nil {
t.Fatalf("err: %s", err)
}
for !c.Exited() {
time.Sleep(10 * time.Millisecond)
}
if !strings.Contains(stderr.String(), "HELLO\n") {
t.Fatalf("bad log data: '%s'", stderr.String())
}
if !strings.Contains(stderr.String(), "WORLD\n") {
t.Fatalf("bad log data: '%s'", stderr.String())
}
}
func TestClient_Stdin(t *testing.T) {
// Overwrite stdin for this test with a temporary file
tf, err := ioutil.TempFile("", "terraform")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err = tf.WriteString("hello"); err != nil {
t.Fatalf("error: %s", err)
}
if err = tf.Sync(); err != nil {
t.Fatalf("error: %s", err)
}
if _, err = tf.Seek(0, 0); err != nil {
t.Fatalf("error: %s", err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }()
os.Stdin = tf
process := helperProcess("stdin")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
_, err = c.Start()
if err != nil {
t.Fatalf("error: %s", err)
}
for {
if c.Exited() {
break
}
time.Sleep(50 * time.Millisecond)
}
if !process.ProcessState.Success() {
t.Fatal("process didn't exit cleanly")
}
}

View File

@ -1,10 +1,13 @@
// The plugin package exposes functions and helpers for communicating to
// Terraform plugins which are implemented as standalone binary applications.
//
// plugin.Client fully manages the lifecycle of executing the application,
// connecting to it, and returning the RPC client and service names for
// connecting to it using the terraform/rpc package.
//
// plugin.Serve fully manages listeners to expose an RPC server from a binary
// that plugin.Client can connect to.
package plugin
import (
"github.com/hashicorp/go-plugin"
)
// See serve.go for serving plugins
// PluginMap should be used by clients for the map of plugins.
var PluginMap = map[string]plugin.Plugin{
"provider": &ResourceProviderPlugin{},
"provisioner": &ResourceProvisionerPlugin{},
}

View File

@ -1,107 +1,16 @@
package plugin
import (
"fmt"
"log"
"os"
"os/exec"
"testing"
"time"
tfrpc "github.com/hashicorp/terraform/rpc"
"github.com/hashicorp/terraform/terraform"
)
func helperProcess(s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
env := []string{
"GO_WANT_HELPER_PROCESS=1",
"TF_PLUGIN_MIN_PORT=10000",
"TF_PLUGIN_MAX_PORT=25000",
}
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = append(env, os.Environ()...)
return cmd
}
// This is not a real test. This is just a helper process kicked off by
// tests.
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "bad-version":
fmt.Printf("%s1|tcp|:1234\n", APIVersion)
<-make(chan int)
case "resource-provider":
Serve(&ServeOpts{
ProviderFunc: testProviderFixed(new(terraform.MockResourceProvider)),
})
case "resource-provisioner":
Serve(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(
new(terraform.MockResourceProvisioner)),
})
case "invalid-rpc-address":
fmt.Println("lolinvalid")
case "mock":
fmt.Printf("%s|tcp|:1234\n", APIVersion)
<-make(chan int)
case "start-timeout":
time.Sleep(1 * time.Minute)
os.Exit(1)
case "stderr":
fmt.Printf("%s|tcp|:1234\n", APIVersion)
log.Println("HELLO")
log.Println("WORLD")
case "stdin":
fmt.Printf("%s|tcp|:1234\n", APIVersion)
data := make([]byte, 5)
if _, err := os.Stdin.Read(data); err != nil {
log.Printf("stdin read error: %s", err)
os.Exit(100)
}
if string(data) == "hello" {
os.Exit(0)
}
os.Exit(1)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
os.Exit(2)
}
}
func testProviderFixed(p terraform.ResourceProvider) tfrpc.ProviderFunc {
func testProviderFixed(p terraform.ResourceProvider) ProviderFunc {
return func() terraform.ResourceProvider {
return p
}
}
func testProvisionerFixed(p terraform.ResourceProvisioner) tfrpc.ProvisionerFunc {
func testProvisionerFixed(p terraform.ResourceProvisioner) ProvisionerFunc {
return func() terraform.ResourceProvisioner {
return p
}

View File

@ -1,24 +1,38 @@
package rpc
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
// ResourceProviderPlugin is the plugin.Plugin implementation.
type ResourceProviderPlugin struct {
F func() terraform.ResourceProvider
}
func (p *ResourceProviderPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
return &ResourceProviderServer{Broker: b, Provider: p.F()}, nil
}
func (p *ResourceProviderPlugin) Client(
b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ResourceProvider{Broker: b, Client: c}, nil
}
// ResourceProvider is an implementation of terraform.ResourceProvider
// that communicates over RPC.
type ResourceProvider struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Client *rpc.Client
Name string
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
id := p.Broker.NextId()
go acceptAndServe(p.Broker, id, "UIInput", &UIInputServer{
go p.Broker.AcceptAndServe(id, &UIInputServer{
UIInput: input,
})
@ -28,7 +42,7 @@ func (p *ResourceProvider) Input(
Config: c,
}
err := p.Client.Call(p.Name+".Input", &args, &resp)
err := p.Client.Call("Plugin.Input", &args, &resp)
if err != nil {
return nil, err
}
@ -46,7 +60,7 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er
Config: c,
}
err := p.Client.Call(p.Name+".Validate", &args, &resp)
err := p.Client.Call("Plugin.Validate", &args, &resp)
if err != nil {
return nil, []error{err}
}
@ -70,7 +84,7 @@ func (p *ResourceProvider) ValidateResource(
Type: t,
}
err := p.Client.Call(p.Name+".ValidateResource", &args, &resp)
err := p.Client.Call("Plugin.ValidateResource", &args, &resp)
if err != nil {
return nil, []error{err}
}
@ -88,7 +102,7 @@ func (p *ResourceProvider) ValidateResource(
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
var resp ResourceProviderConfigureResponse
err := p.Client.Call(p.Name+".Configure", c, &resp)
err := p.Client.Call("Plugin.Configure", c, &resp)
if err != nil {
return err
}
@ -110,7 +124,7 @@ func (p *ResourceProvider) Apply(
Diff: d,
}
err := p.Client.Call(p.Name+".Apply", args, &resp)
err := p.Client.Call("Plugin.Apply", args, &resp)
if err != nil {
return nil, err
}
@ -131,7 +145,7 @@ func (p *ResourceProvider) Diff(
State: s,
Config: c,
}
err := p.Client.Call(p.Name+".Diff", args, &resp)
err := p.Client.Call("Plugin.Diff", args, &resp)
if err != nil {
return nil, err
}
@ -151,7 +165,7 @@ func (p *ResourceProvider) Refresh(
State: s,
}
err := p.Client.Call(p.Name+".Refresh", args, &resp)
err := p.Client.Call("Plugin.Refresh", args, &resp)
if err != nil {
return nil, err
}
@ -165,7 +179,7 @@ func (p *ResourceProvider) Refresh(
func (p *ResourceProvider) Resources() []terraform.ResourceType {
var result []terraform.ResourceType
err := p.Client.Call(p.Name+".Resources", new(interface{}), &result)
err := p.Client.Call("Plugin.Resources", new(interface{}), &result)
if err != nil {
// TODO: panic, log, what?
return nil
@ -181,12 +195,12 @@ func (p *ResourceProvider) Close() error {
// ResourceProviderServer is a net/rpc compatible structure for serving
// a ResourceProvider. This should not be used directly.
type ResourceProviderServer struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Provider terraform.ResourceProvider
}
type ResourceProviderConfigureResponse struct {
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderInputArgs struct {
@ -196,7 +210,7 @@ type ResourceProviderInputArgs struct {
type ResourceProviderInputResponse struct {
Config *terraform.ResourceConfig
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderApplyArgs struct {
@ -207,7 +221,7 @@ type ResourceProviderApplyArgs struct {
type ResourceProviderApplyResponse struct {
State *terraform.InstanceState
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderDiffArgs struct {
@ -218,7 +232,7 @@ type ResourceProviderDiffArgs struct {
type ResourceProviderDiffResponse struct {
Diff *terraform.InstanceDiff
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderRefreshArgs struct {
@ -228,7 +242,7 @@ type ResourceProviderRefreshArgs struct {
type ResourceProviderRefreshResponse struct {
State *terraform.InstanceState
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderValidateArgs struct {
@ -237,7 +251,7 @@ type ResourceProviderValidateArgs struct {
type ResourceProviderValidateResponse struct {
Warnings []string
Errors []*BasicError
Errors []*plugin.BasicError
}
type ResourceProviderValidateResourceArgs struct {
@ -247,7 +261,7 @@ type ResourceProviderValidateResourceArgs struct {
type ResourceProviderValidateResourceResponse struct {
Warnings []string
Errors []*BasicError
Errors []*plugin.BasicError
}
func (s *ResourceProviderServer) Input(
@ -256,22 +270,19 @@ func (s *ResourceProviderServer) Input(
conn, err := s.Broker.Dial(args.InputId)
if err != nil {
*reply = ResourceProviderInputResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
client := rpc.NewClient(conn)
defer client.Close()
input := &UIInput{
Client: client,
Name: "UIInput",
}
input := &UIInput{Client: client}
config, err := s.Provider.Input(input, args.Config)
*reply = ResourceProviderInputResponse{
Config: config,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
@ -281,9 +292,9 @@ func (s *ResourceProviderServer) Validate(
args *ResourceProviderValidateArgs,
reply *ResourceProviderValidateResponse) error {
warns, errs := s.Provider.Validate(args.Config)
berrs := make([]*BasicError, len(errs))
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = NewBasicError(err)
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProviderValidateResponse{
Warnings: warns,
@ -296,9 +307,9 @@ func (s *ResourceProviderServer) ValidateResource(
args *ResourceProviderValidateResourceArgs,
reply *ResourceProviderValidateResourceResponse) error {
warns, errs := s.Provider.ValidateResource(args.Type, args.Config)
berrs := make([]*BasicError, len(errs))
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = NewBasicError(err)
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProviderValidateResourceResponse{
Warnings: warns,
@ -312,7 +323,7 @@ func (s *ResourceProviderServer) Configure(
reply *ResourceProviderConfigureResponse) error {
err := s.Provider.Configure(config)
*reply = ResourceProviderConfigureResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -323,7 +334,7 @@ func (s *ResourceProviderServer) Apply(
state, err := s.Provider.Apply(args.Info, args.State, args.Diff)
*result = ResourceProviderApplyResponse{
State: state,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -334,7 +345,7 @@ func (s *ResourceProviderServer) Diff(
diff, err := s.Provider.Diff(args.Info, args.State, args.Config)
*result = ResourceProviderDiffResponse{
Diff: diff,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -345,7 +356,7 @@ func (s *ResourceProviderServer) Refresh(
newState, err := s.Provider.Refresh(args.Info, args.State)
*result = ResourceProviderRefreshResponse{
State: newState,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}

View File

@ -1,15 +1,624 @@
package plugin
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvider(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: helperProcess("resource-provider")})
defer c.Kill()
func TestResourceProvider_impl(t *testing.T) {
var _ plugin.Plugin = new(ResourceProviderPlugin)
var _ terraform.ResourceProvider = new(ResourceProvider)
}
_, err := c.Client()
func TestResourceProvider_input(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvider)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("should not have error: %s", err)
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
input := new(terraform.MockUIInput)
expected := &terraform.ResourceConfig{
Raw: map[string]interface{}{"bar": "baz"},
}
p.InputReturnConfig = expected
// Input
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
actual, err := provider.Input(input, config)
if !p.InputCalled {
t.Fatal("input should be called")
}
if !reflect.DeepEqual(p.InputConfig, config) {
t.Fatalf("bad: %#v", p.InputConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceProvider_configure(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvider)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_configure_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ConfigureReturnError = errors.New("foo")
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e == nil {
t.Fatal("should have error")
}
if e.Error() != "foo" {
t.Fatalf("bad: %s", e)
}
}
func TestResourceProvider_configure_warnings(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_apply(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ApplyReturn = &terraform.InstanceState{
ID: "bob",
}
// Apply
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
diff := &terraform.InstanceDiff{}
newState, err := provider.Apply(info, state, diff)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyDiff, diff) {
t.Fatalf("bad: %#v", p.ApplyDiff)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.ApplyReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_diff(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.DiffReturn = &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
}
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.DiffReturn, diff) {
t.Fatalf("bad: %#v", diff)
}
}
func TestResourceProvider_diff_error(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.DiffReturnError = errors.New("foo")
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err == nil {
t.Fatal("should have error")
}
if diff != nil {
t.Fatal("should not have diff")
}
}
func TestResourceProvider_refresh(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.RefreshReturn = &terraform.InstanceState{
ID: "bob",
}
// Refresh
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
newState, err := provider.Refresh(info, state)
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
if !reflect.DeepEqual(p.RefreshState, state) {
t.Fatalf("bad: %#v", p.RefreshState)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.RefreshReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_resources(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
expected := []terraform.ResourceType{
{"foo"},
{"bar"},
}
p.ResourcesReturn = expected
// Resources
result := provider.Resources()
if !p.ResourcesCalled {
t.Fatal("resources should be called")
}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("bad: %#v", result)
}
}
func TestResourceProvider_validate(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateReturnErrors = []error{errors.New("foo")}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateReturnWarns = []string{"foo"}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_validateResource(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateResourceReturnErrors = []error{errors.New("foo")}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateResourceReturnWarns = []string{"foo"}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_close(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
var iface interface{} = provider
pCloser, ok := iface.(terraform.ResourceProviderCloser)
if !ok {
t.Fatal("should be a ResourceProviderCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provider: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
err = provider.Configure(&terraform.ResourceConfig{})
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -1,17 +1,31 @@
package rpc
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
// ResourceProvisionerPlugin is the plugin.Plugin implementation.
type ResourceProvisionerPlugin struct {
F func() terraform.ResourceProvisioner
}
func (p *ResourceProvisionerPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
return &ResourceProvisionerServer{Broker: b, Provisioner: p.F()}, nil
}
func (p *ResourceProvisionerPlugin) Client(
b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ResourceProvisioner{Broker: b, Client: c}, nil
}
// ResourceProvisioner is an implementation of terraform.ResourceProvisioner
// that communicates over RPC.
type ResourceProvisioner struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Client *rpc.Client
Name string
}
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) {
@ -20,7 +34,7 @@ func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, [
Config: c,
}
err := p.Client.Call(p.Name+".Validate", &args, &resp)
err := p.Client.Call("Plugin.Validate", &args, &resp)
if err != nil {
return nil, []error{err}
}
@ -41,7 +55,7 @@ func (p *ResourceProvisioner) Apply(
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
id := p.Broker.NextId()
go acceptAndServe(p.Broker, id, "UIOutput", &UIOutputServer{
go p.Broker.AcceptAndServe(id, &UIOutputServer{
UIOutput: output,
})
@ -52,7 +66,7 @@ func (p *ResourceProvisioner) Apply(
Config: c,
}
err := p.Client.Call(p.Name+".Apply", args, &resp)
err := p.Client.Call("Plugin.Apply", args, &resp)
if err != nil {
return err
}
@ -73,7 +87,7 @@ type ResourceProvisionerValidateArgs struct {
type ResourceProvisionerValidateResponse struct {
Warnings []string
Errors []*BasicError
Errors []*plugin.BasicError
}
type ResourceProvisionerApplyArgs struct {
@ -83,13 +97,13 @@ type ResourceProvisionerApplyArgs struct {
}
type ResourceProvisionerApplyResponse struct {
Error *BasicError
Error *plugin.BasicError
}
// ResourceProvisionerServer is a net/rpc compatible structure for serving
// a ResourceProvisioner. This should not be used directly.
type ResourceProvisionerServer struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Provisioner terraform.ResourceProvisioner
}
@ -99,21 +113,18 @@ func (s *ResourceProvisionerServer) Apply(
conn, err := s.Broker.Dial(args.OutputId)
if err != nil {
*result = ResourceProvisionerApplyResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
client := rpc.NewClient(conn)
defer client.Close()
output := &UIOutput{
Client: client,
Name: "UIOutput",
}
output := &UIOutput{Client: client}
err = s.Provisioner.Apply(output, args.State, args.Config)
*result = ResourceProvisionerApplyResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -122,9 +133,9 @@ func (s *ResourceProvisionerServer) Validate(
args *ResourceProvisionerValidateArgs,
reply *ResourceProvisionerValidateResponse) error {
warns, errs := s.Provisioner.Validate(args.Config)
berrs := make([]*BasicError, len(errs))
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = NewBasicError(err)
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProvisionerValidateResponse{
Warnings: warns,

View File

@ -1,15 +1,193 @@
package plugin
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvisioner(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: helperProcess("resource-provisioner")})
defer c.Kill()
func TestResourceProvisioner_impl(t *testing.T) {
var _ plugin.Plugin = new(ResourceProvisionerPlugin)
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
_, err := c.Client()
func TestResourceProvisioner_apply(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("should not have error: %s", err)
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
// Apply
output := &terraform.MockUIOutput{}
state := &terraform.InstanceState{}
conf := &terraform.ResourceConfig{}
err = provisioner.Apply(output, state, conf)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyConfig, conf) {
t.Fatalf("bad: %#v", p.ApplyConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
}
func TestResourceProvisioner_validate(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_errors(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
p.ValidateReturnErrors = []error{errors.New("foo")}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_warns(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
p.ValidateReturnWarns = []string{"foo"}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvisioner_close(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
pCloser, ok := raw.(terraform.ResourceProvisionerCloser)
if !ok {
t.Fatal("should be a ResourceProvisionerCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provisioner: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
o := &terraform.MockUIOutput{}
s := &terraform.InstanceState{}
c := &terraform.ResourceConfig{}
err = provisioner.Apply(o, s, c)
if err == nil {
t.Fatal("should have error")
}
}

47
plugin/serve.go Normal file
View File

@ -0,0 +1,47 @@
package plugin
import (
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
// The constants below are the names of the plugins that can be dispensed
// from the plugin server.
const (
ProviderPluginName = "provider"
ProvisionerPluginName = "provisioner"
)
// Handshake is the HandshakeConfig used to configure clients and servers.
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2",
}
type ProviderFunc func() terraform.ResourceProvider
type ProvisionerFunc func() terraform.ResourceProvisioner
// ServeOpts are the configurations to serve a plugin.
type ServeOpts struct {
ProviderFunc ProviderFunc
ProvisionerFunc ProvisionerFunc
}
// Serve serves a plugin. This function never returns and should be the final
// function called in the main function of the plugin.
func Serve(opts *ServeOpts) {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: Handshake,
Plugins: pluginMap(opts),
})
}
// pluginMap returns the map[string]plugin.Plugin to use for configuring a plugin
// server or client.
func pluginMap(opts *ServeOpts) map[string]plugin.Plugin {
return map[string]plugin.Plugin{
"provider": &ResourceProviderPlugin{F: opts.ProviderFunc},
"provisioner": &ResourceProvisionerPlugin{F: opts.ProvisionerFunc},
}
}

View File

@ -1,138 +0,0 @@
package plugin
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"sync/atomic"
tfrpc "github.com/hashicorp/terraform/rpc"
)
// The APIVersion is outputted along with the RPC address. The plugin
// client validates this API version and will show an error if it doesn't
// know how to speak it.
const APIVersion = "2"
// The "magic cookie" is used to verify that the user intended to
// actually run this binary. If this cookie isn't present as an
// environmental variable, then we bail out early with an error.
const MagicCookieKey = "TF_PLUGIN_MAGIC_COOKIE"
const MagicCookieValue = "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2"
// ServeOpts configures what sorts of plugins are served.
type ServeOpts struct {
ProviderFunc tfrpc.ProviderFunc
ProvisionerFunc tfrpc.ProvisionerFunc
}
// Serve serves the plugins given by ServeOpts.
//
// Serve doesn't return until the plugin is done being executed. Any
// errors will be outputted to the log.
func Serve(opts *ServeOpts) {
// First check the cookie
if os.Getenv(MagicCookieKey) != MagicCookieValue {
fmt.Fprintf(os.Stderr,
"This binary is a Terraform plugin. These are not meant to be\n"+
"executed directly. Please execute `terraform`, which will load\n"+
"any plugins automatically.\n")
os.Exit(1)
}
// Register a listener so we can accept a connection
listener, err := serverListener()
if err != nil {
log.Printf("[ERR] plugin init: %s", err)
return
}
defer listener.Close()
// Create the RPC server to dispense
server := &tfrpc.Server{
ProviderFunc: opts.ProviderFunc,
ProvisionerFunc: opts.ProvisionerFunc,
}
// Output the address and service name to stdout so that Terraform
// core can bring it up.
log.Printf("Plugin address: %s %s\n",
listener.Addr().Network(), listener.Addr().String())
fmt.Printf("%s|%s|%s\n",
APIVersion,
listener.Addr().Network(),
listener.Addr().String())
os.Stdout.Sync()
// Eat the interrupts
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go func() {
var count int32 = 0
for {
<-ch
newCount := atomic.AddInt32(&count, 1)
log.Printf(
"Received interrupt signal (count: %d). Ignoring.",
newCount)
}
}()
// Serve
server.Accept(listener)
}
func serverListener() (net.Listener, error) {
if runtime.GOOS == "windows" {
return serverListener_tcp()
}
return serverListener_unix()
}
func serverListener_tcp() (net.Listener, error) {
minPort, err := strconv.ParseInt(os.Getenv("TF_PLUGIN_MIN_PORT"), 10, 32)
if err != nil {
return nil, err
}
maxPort, err := strconv.ParseInt(os.Getenv("TF_PLUGIN_MAX_PORT"), 10, 32)
if err != nil {
return nil, err
}
for port := minPort; port <= maxPort; port++ {
address := fmt.Sprintf("127.0.0.1:%d", port)
listener, err := net.Listen("tcp", address)
if err == nil {
return listener, nil
}
}
return nil, errors.New("Couldn't bind plugin TCP listener")
}
func serverListener_unix() (net.Listener, error) {
tf, err := ioutil.TempFile("", "tf-plugin")
if err != nil {
return nil, err
}
path := tf.Name()
// Close the file and remove it because it has to not exist for
// the domain socket.
if err := tf.Close(); err != nil {
return nil, err
}
if err := os.Remove(path); err != nil {
return nil, err
}
return net.Listen("unix", path)
}

View File

@ -1,8 +1,9 @@
package rpc
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
@ -10,12 +11,11 @@ import (
// over RPC.
type UIInput struct {
Client *rpc.Client
Name string
}
func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
var resp UIInputInputResponse
err := i.Client.Call(i.Name+".Input", opts, &resp)
err := i.Client.Call("Plugin.Input", opts, &resp)
if err != nil {
return "", err
}
@ -29,7 +29,7 @@ func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
type UIInputInputResponse struct {
Value string
Error *BasicError
Error *plugin.BasicError
}
// UIInputServer is a net/rpc compatible structure for serving
@ -44,7 +44,7 @@ func (s *UIInputServer) Input(
value, err := s.UIInput.Input(opts)
*reply = UIInputInputResponse{
Value: value,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil

View File

@ -1,9 +1,10 @@
package rpc
package plugin
import (
"reflect"
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
@ -12,20 +13,20 @@ func TestUIInput_impl(t *testing.T) {
}
func TestUIInput_input(t *testing.T) {
client, server := testClientServer(t)
client, server := plugin.TestRPCConn(t)
defer client.Close()
i := new(terraform.MockUIInput)
i.InputReturnString = "foo"
err := server.RegisterName("UIInput", &UIInputServer{
err := server.RegisterName("Plugin", &UIInputServer{
UIInput: i,
})
if err != nil {
t.Fatalf("err: %s", err)
}
input := &UIInput{Client: client, Name: "UIInput"}
input := &UIInput{Client: client}
opts := &terraform.InputOpts{
Id: "foo",

View File

@ -1,4 +1,4 @@
package rpc
package plugin
import (
"net/rpc"
@ -10,11 +10,10 @@ import (
// over RPC.
type UIOutput struct {
Client *rpc.Client
Name string
}
func (o *UIOutput) Output(v string) {
o.Client.Call(o.Name+".Output", v, new(interface{}))
o.Client.Call("Plugin.Output", v, new(interface{}))
}
// UIOutputServer is the RPC server for serving UIOutput.

View File

@ -1,8 +1,9 @@
package rpc
package plugin
import (
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
@ -11,19 +12,19 @@ func TestUIOutput_impl(t *testing.T) {
}
func TestUIOutput_input(t *testing.T) {
client, server := testClientServer(t)
client, server := plugin.TestRPCConn(t)
defer client.Close()
o := new(terraform.MockUIOutput)
err := server.RegisterName("UIOutput", &UIOutputServer{
err := server.RegisterName("Plugin", &UIOutputServer{
UIOutput: o,
})
if err != nil {
t.Fatalf("err: %s", err)
}
output := &UIOutput{Client: client, Name: "UIOutput"}
output := &UIOutput{Client: client}
output.Output("foo")
if !o.OutputCalled {
t.Fatal("output should be called")

View File

@ -1,108 +0,0 @@
package rpc
import (
"io"
"net"
"net/rpc"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/yamux"
)
// Client connects to a Server in order to request plugin implementations
// for Terraform.
type Client struct {
broker *muxBroker
control *rpc.Client
}
// Dial opens a connection to a Terraform RPC server and returns a client.
func Dial(network, address string) (*Client, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
// Make sure to set keep alive so that the connection doesn't die
tcpConn.SetKeepAlive(true)
}
return NewClient(conn)
}
// NewClient creates a client from an already-open connection-like value.
// Dial is typically used instead.
func NewClient(conn io.ReadWriteCloser) (*Client, error) {
// Create the yamux client so we can multiplex
mux, err := yamux.Client(conn, nil)
if err != nil {
conn.Close()
return nil, err
}
// Connect to the control stream.
control, err := mux.Open()
if err != nil {
mux.Close()
return nil, err
}
// Create the broker and start it up
broker := newMuxBroker(mux)
go broker.Run()
// Build the client using our broker and control channel.
return &Client{
broker: broker,
control: rpc.NewClient(control),
}, nil
}
// Close closes the connection. The client is no longer usable after this
// is called.
func (c *Client) Close() error {
if err := c.control.Close(); err != nil {
return err
}
return c.broker.Close()
}
func (c *Client) ResourceProvider() (terraform.ResourceProvider, error) {
var id uint32
if err := c.control.Call(
"Dispenser.ResourceProvider", new(interface{}), &id); err != nil {
return nil, err
}
conn, err := c.broker.Dial(id)
if err != nil {
return nil, err
}
return &ResourceProvider{
Broker: c.broker,
Client: rpc.NewClient(conn),
Name: "ResourceProvider",
}, nil
}
func (c *Client) ResourceProvisioner() (terraform.ResourceProvisioner, error) {
var id uint32
if err := c.control.Call(
"Dispenser.ResourceProvisioner", new(interface{}), &id); err != nil {
return nil, err
}
conn, err := c.broker.Dial(id)
if err != nil {
return nil, err
}
return &ResourceProvisioner{
Broker: c.broker,
Client: rpc.NewClient(conn),
Name: "ResourceProvisioner",
}, nil
}

View File

@ -1,76 +0,0 @@
package rpc
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestClient_ResourceProvider(t *testing.T) {
clientConn, serverConn := testConn(t)
p := new(terraform.MockResourceProvider)
server := &Server{ProviderFunc: testProviderFixed(p)}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
defer client.Close()
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestClient_ResourceProvisioner(t *testing.T) {
clientConn, serverConn := testConn(t)
p := new(terraform.MockResourceProvisioner)
server := &Server{ProvisionerFunc: testProvisionerFixed(p)}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
defer client.Close()
provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}
// Apply
output := &terraform.MockUIOutput{}
state := &terraform.InstanceState{}
conf := &terraform.ResourceConfig{}
err = provisioner.Apply(output, state, conf)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyConfig, conf) {
t.Fatalf("bad: %#v", p.ApplyConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
}

View File

@ -1,26 +0,0 @@
package rpc
import (
"errors"
"testing"
)
func TestBasicError_ImplementsError(t *testing.T) {
var _ error = new(BasicError)
}
func TestBasicError_MatchesMessage(t *testing.T) {
err := errors.New("foo")
wrapped := NewBasicError(err)
if wrapped.Error() != err.Error() {
t.Fatalf("bad: %#v", wrapped.Error())
}
}
func TestNewBasicError_nil(t *testing.T) {
r := NewBasicError(nil)
if r != nil {
t.Fatalf("bad: %#v", r)
}
}

View File

@ -1,518 +0,0 @@
package rpc
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(ResourceProvider)
}
func TestResourceProvider_input(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProviderFunc().(*terraform.MockResourceProvider)
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
input := new(terraform.MockUIInput)
expected := &terraform.ResourceConfig{
Raw: map[string]interface{}{"bar": "baz"},
}
p.InputReturnConfig = expected
// Input
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
actual, err := provider.Input(input, config)
if !p.InputCalled {
t.Fatal("input should be called")
}
if !reflect.DeepEqual(p.InputConfig, config) {
t.Fatalf("bad: %#v", p.InputConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceProvider_configure(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProviderFunc().(*terraform.MockResourceProvider)
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_configure_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.ConfigureReturnError = errors.New("foo")
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e == nil {
t.Fatal("should have error")
}
if e.Error() != "foo" {
t.Fatalf("bad: %s", e)
}
}
func TestResourceProvider_configure_warnings(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_apply(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.ApplyReturn = &terraform.InstanceState{
ID: "bob",
}
// Apply
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
diff := &terraform.InstanceDiff{}
newState, err := provider.Apply(info, state, diff)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyDiff, diff) {
t.Fatalf("bad: %#v", p.ApplyDiff)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.ApplyReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_diff(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.DiffReturn = &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
}
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.DiffReturn, diff) {
t.Fatalf("bad: %#v", diff)
}
}
func TestResourceProvider_diff_error(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.DiffReturnError = errors.New("foo")
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err == nil {
t.Fatal("should have error")
}
if diff != nil {
t.Fatal("should not have diff")
}
}
func TestResourceProvider_refresh(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.RefreshReturn = &terraform.InstanceState{
ID: "bob",
}
// Refresh
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
newState, err := provider.Refresh(info, state)
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
if !reflect.DeepEqual(p.RefreshState, state) {
t.Fatalf("bad: %#v", p.RefreshState)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.RefreshReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_resources(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
expected := []terraform.ResourceType{
{"foo"},
{"bar"},
}
p.ResourcesReturn = expected
// Resources
result := provider.Resources()
if !p.ResourcesCalled {
t.Fatal("resources should be called")
}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("bad: %#v", result)
}
}
func TestResourceProvider_validate(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateReturnErrors = []error{errors.New("foo")}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateReturnWarns = []string{"foo"}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_validateResource(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateResourceReturnErrors = []error{errors.New("foo")}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateResourceReturnWarns = []string{"foo"}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_close(t *testing.T) {
client, _ := testNewClientServer(t)
defer client.Close()
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
var p interface{}
p = provider
pCloser, ok := p.(terraform.ResourceProviderCloser)
if !ok {
t.Fatal("should be a ResourceProviderCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provider: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
err = provider.Configure(&terraform.ResourceConfig{})
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -1,165 +0,0 @@
package rpc
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
func TestResourceProvisioner_apply(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProvisionerFunc().(*terraform.MockResourceProvisioner)
provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}
// Apply
output := &terraform.MockUIOutput{}
state := &terraform.InstanceState{}
conf := &terraform.ResourceConfig{}
err = provisioner.Apply(output, state, conf)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyConfig, conf) {
t.Fatalf("bad: %#v", p.ApplyConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
}
func TestResourceProvisioner_validate(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := &ResourceProvisioner{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_errors(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
p.ValidateReturnErrors = []error{errors.New("foo")}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := &ResourceProvisioner{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_warns(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
p.ValidateReturnWarns = []string{"foo"}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := &ResourceProvisioner{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvisioner_close(t *testing.T) {
client, _ := testNewClientServer(t)
defer client.Close()
provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}
var p interface{}
p = provisioner
pCloser, ok := p.(terraform.ResourceProvisionerCloser)
if !ok {
t.Fatal("should be a ResourceProvisionerCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provisioner: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
o := &terraform.MockUIOutput{}
s := &terraform.InstanceState{}
c := &terraform.ResourceConfig{}
err = provisioner.Apply(o, s, c)
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -1,35 +0,0 @@
package rpc
import (
"errors"
"fmt"
"net/rpc"
"sync"
"github.com/hashicorp/terraform/terraform"
)
// nextId is the next ID to use for names registered.
var nextId uint32 = 0
var nextLock sync.Mutex
// Register registers a Terraform thing with the RPC server and returns
// the name it is registered under.
func Register(server *rpc.Server, thing interface{}) (name string, err error) {
nextLock.Lock()
defer nextLock.Unlock()
switch t := thing.(type) {
case terraform.ResourceProvider:
name = fmt.Sprintf("Terraform%d", nextId)
err = server.RegisterName(name, &ResourceProviderServer{Provider: t})
case terraform.ResourceProvisioner:
name = fmt.Sprintf("Terraform%d", nextId)
err = server.RegisterName(name, &ResourceProvisionerServer{Provisioner: t})
default:
return "", errors.New("Unknown type to register for RPC server.")
}
nextId += 1
return
}

View File

@ -1,77 +0,0 @@
package rpc
import (
"net"
"net/rpc"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func testConn(t *testing.T) (net.Conn, net.Conn) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %s", err)
}
var serverConn net.Conn
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
defer l.Close()
var err error
serverConn, err = l.Accept()
if err != nil {
t.Fatalf("err: %s", err)
}
}()
clientConn, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("err: %s", err)
}
<-doneCh
return clientConn, serverConn
}
func testClientServer(t *testing.T) (*rpc.Client, *rpc.Server) {
clientConn, serverConn := testConn(t)
server := rpc.NewServer()
go server.ServeConn(serverConn)
client := rpc.NewClient(clientConn)
return client, server
}
func testNewClientServer(t *testing.T) (*Client, *Server) {
clientConn, serverConn := testConn(t)
server := &Server{
ProviderFunc: testProviderFixed(new(terraform.MockResourceProvider)),
ProvisionerFunc: testProvisionerFixed(
new(terraform.MockResourceProvisioner)),
}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
return client, server
}
func testProviderFixed(p terraform.ResourceProvider) ProviderFunc {
return func() terraform.ResourceProvider {
return p
}
}
func testProvisionerFixed(p terraform.ResourceProvisioner) ProvisionerFunc {
return func() terraform.ResourceProvisioner {
return p
}
}

View File

@ -1,147 +0,0 @@
package rpc
import (
"io"
"log"
"net"
"net/rpc"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/yamux"
)
// Server listens for network connections and then dispenses interface
// implementations for Terraform over net/rpc.
type Server struct {
ProviderFunc ProviderFunc
ProvisionerFunc ProvisionerFunc
}
// ProviderFunc creates terraform.ResourceProviders when they're requested
// from the server.
type ProviderFunc func() terraform.ResourceProvider
// ProvisionerFunc creates terraform.ResourceProvisioners when they're requested
// from the server.
type ProvisionerFunc func() terraform.ResourceProvisioner
// Accept accepts connections on a listener and serves requests for
// each incoming connection. Accept blocks; the caller typically invokes
// it in a go statement.
func (s *Server) Accept(lis net.Listener) {
for {
conn, err := lis.Accept()
if err != nil {
log.Printf("[ERR] plugin server: %s", err)
return
}
go s.ServeConn(conn)
}
}
// ServeConn runs a single connection.
//
// ServeConn blocks, serving the connection until the client hangs up.
func (s *Server) ServeConn(conn io.ReadWriteCloser) {
// First create the yamux server to wrap this connection
mux, err := yamux.Server(conn, nil)
if err != nil {
conn.Close()
log.Printf("[ERR] plugin: %s", err)
return
}
// Accept the control connection
control, err := mux.Accept()
if err != nil {
mux.Close()
log.Printf("[ERR] plugin: %s", err)
return
}
// Create the broker and start it up
broker := newMuxBroker(mux)
go broker.Run()
// Use the control connection to build the dispenser and serve the
// connection.
server := rpc.NewServer()
server.RegisterName("Dispenser", &dispenseServer{
ProviderFunc: s.ProviderFunc,
ProvisionerFunc: s.ProvisionerFunc,
broker: broker,
})
server.ServeConn(control)
}
// dispenseServer dispenses variousinterface implementations for Terraform.
type dispenseServer struct {
ProviderFunc ProviderFunc
ProvisionerFunc ProvisionerFunc
broker *muxBroker
}
func (d *dispenseServer) ResourceProvider(
args interface{}, response *uint32) error {
id := d.broker.NextId()
*response = id
go func() {
conn, err := d.broker.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)
return
}
serve(conn, "ResourceProvider", &ResourceProviderServer{
Broker: d.broker,
Provider: d.ProviderFunc(),
})
}()
return nil
}
func (d *dispenseServer) ResourceProvisioner(
args interface{}, response *uint32) error {
id := d.broker.NextId()
*response = id
go func() {
conn, err := d.broker.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)
return
}
serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{
Broker: d.broker,
Provisioner: d.ProvisionerFunc(),
})
}()
return nil
}
func acceptAndServe(mux *muxBroker, id uint32, n string, v interface{}) {
conn, err := mux.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin acceptAndServe: %s", err)
return
}
serve(conn, n, v)
}
func serve(conn io.ReadWriteCloser, name string, v interface{}) {
server := rpc.NewServer()
if err := server.RegisterName(name, v); err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)
return
}
server.ServeConn(conn)
}

View File

@ -47,16 +47,8 @@ gox \
-os="${XC_OS}" \
-arch="${XC_ARCH}" \
-ldflags "${LD_FLAGS}" \
-output "pkg/{{.OS}}_{{.Arch}}/terraform-{{.Dir}}" \
$(go list ./... | grep -v /vendor/)
# Make sure "terraform-terraform" is renamed properly
for PLATFORM in $(find ./pkg -mindepth 1 -maxdepth 1 -type d); do
set +e
mv ${PLATFORM}/terraform-terraform.exe ${PLATFORM}/terraform.exe 2>/dev/null
mv ${PLATFORM}/terraform-terraform ${PLATFORM}/terraform 2>/dev/null
set -e
done
-output "pkg/{{.OS}}_{{.Arch}}/terraform" \
.
# Move all the compiled things to the $GOPATH/bin
GOPATH=${GOPATH:-$(go env GOPATH)}

283
scripts/generate-plugins.go Normal file
View File

@ -0,0 +1,283 @@
// Generate Plugins is a small program that updates the lists of plugins in
// command/internal_plugin_list.go so they will be compiled into the main
// terraform binary.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
)
const target = "command/internal_plugin_list.go"
func main() {
wd, _ := os.Getwd()
if filepath.Base(wd) != "terraform" {
log.Fatalf("This program must be invoked in the terraform project root; in %s", wd)
}
// Collect all of the data we need about plugins we have in the project
providers, err := discoverProviders()
if err != nil {
log.Fatalf("Failed to discover providers: %s", err)
}
provisioners, err := discoverProvisioners()
if err != nil {
log.Fatalf("Failed to discover provisioners: %s", err)
}
// Do some simple code generation and templating
output := source
output = strings.Replace(output, "IMPORTS", makeImports(providers, provisioners), 1)
output = strings.Replace(output, "PROVIDERS", makeProviderMap(providers), 1)
output = strings.Replace(output, "PROVISIONERS", makeProvisionerMap(provisioners), 1)
// TODO sort the lists of plugins so we are not subjected to random OS ordering of the plugin lists
// Write our generated code to the command/plugin.go file
file, err := os.Create(target)
defer file.Close()
if err != nil {
log.Fatalf("Failed to open %s for writing: %s", target, err)
}
_, err = file.WriteString(output)
if err != nil {
log.Fatalf("Failed writing to %s: %s", target, err)
}
log.Printf("Generated %s", target)
}
type plugin struct {
Package string // Package name from ast remoteexec
PluginName string // Path via deriveName() remote-exec
TypeName string // Type of plugin provisioner
Path string // Relative import path builtin/provisioners/remote-exec
ImportName string // See deriveImport() remoteexecprovisioner
}
// makeProviderMap creates a map of providers like this:
//
// var InternalProviders = map[string]plugin.ProviderFunc{
// "aws": aws.Provider,
// "azurerm": azurerm.Provider,
// "cloudflare": cloudflare.Provider,
func makeProviderMap(items []plugin) string {
output := ""
for _, item := range items {
output += fmt.Sprintf("\t\"%s\": %s.%s,\n", item.PluginName, item.ImportName, item.TypeName)
}
return output
}
// makeProvisionerMap creates a map of provisioners like this:
//
// "file": func() terraform.ResourceProvisioner { return new(file.ResourceProvisioner) },
// "local-exec": func() terraform.ResourceProvisioner { return new(localexec.ResourceProvisioner) },
// "remote-exec": func() terraform.ResourceProvisioner { return new(remoteexec.ResourceProvisioner) },
//
// This is more verbose than the Provider case because there is no corresponding
// Provisioner function.
func makeProvisionerMap(items []plugin) string {
output := ""
for _, item := range items {
output += fmt.Sprintf("\t\"%s\": func() terraform.ResourceProvisioner { return new(%s.%s) },\n", item.PluginName, item.ImportName, item.TypeName)
}
return output
}
func makeImports(providers, provisioners []plugin) string {
plugins := []string{}
for _, provider := range providers {
plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provider.ImportName, filepath.ToSlash(provider.Path)))
}
for _, provisioner := range provisioners {
plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.Path)))
}
// Make things pretty
sort.Strings(plugins)
return strings.Join(plugins, "")
}
// listDirectories recursively lists directories under the specified path
func listDirectories(path string) ([]string, error) {
names := []string{}
items, err := ioutil.ReadDir(path)
if err != nil {
return names, err
}
for _, item := range items {
// We only want directories
if item.IsDir() {
if item.Name() == "test-fixtures" {
continue
}
currentDir := filepath.Join(path, item.Name())
names = append(names, currentDir)
// Do some recursion
subNames, err := listDirectories(currentDir)
if err == nil {
names = append(names, subNames...)
}
}
}
return names, nil
}
// deriveName determines the name of the plugin relative to the specified root
// path.
func deriveName(root, full string) string {
short, _ := filepath.Rel(root, full)
bits := strings.Split(short, string(os.PathSeparator))
return strings.Join(bits, "-")
}
// deriveImport will build a unique import identifier based on packageName and
// the result of deriveName(). This is important for disambigutating between
// providers and provisioners that have the same name. This will be something
// like:
//
// remote-exec -> remoteexecprovisioner
//
// which is long, but is deterministic and unique.
func deriveImport(typeName, derivedName string) string {
return strings.Replace(derivedName, "-", "", -1) + strings.ToLower(typeName)
}
// discoverTypesInPath searches for types of typeID in path using go's ast and
// returns a list of plugins it finds.
func discoverTypesInPath(path, typeID, typeName string) ([]plugin, error) {
pluginTypes := []plugin{}
dirs, err := listDirectories(path)
if err != nil {
return pluginTypes, err
}
for _, dir := range dirs {
fset := token.NewFileSet()
goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors)
if err != nil {
return pluginTypes, fmt.Errorf("Failed parsing directory %s: %s", dir, err)
}
for _, goPackage := range goPackages {
ast.PackageExports(goPackage)
ast.Inspect(goPackage, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
// If we get a function then we will check the function name
// against typeName and the function return type (Results)
// against typeID.
//
// There may be more than one return type but in the target
// case there should only be one. Also the return type is a
// ast.SelectorExpr which means we have multiple nodes.
// We'll read all of them as ast.Ident (identifier), join
// them via . to get a string like terraform.ResourceProvider
// and see if it matches our expected typeID
//
// This is somewhat verbose but prevents us from identifying
// the wrong types if the function name is amiguous or if
// there are other subfolders added later.
if x.Name.Name == typeName && len(x.Type.Results.List) == 1 {
node := x.Type.Results.List[0].Type
typeIdentifiers := []string{}
ast.Inspect(node, func(m ast.Node) bool {
switch y := m.(type) {
case *ast.Ident:
typeIdentifiers = append(typeIdentifiers, y.Name)
}
// We need all of the identifiers to join so we
// can't break early here.
return true
})
if strings.Join(typeIdentifiers, ".") == typeID {
derivedName := deriveName(path, dir)
pluginTypes = append(pluginTypes, plugin{
Package: goPackage.Name,
PluginName: derivedName,
ImportName: deriveImport(x.Name.Name, derivedName),
TypeName: x.Name.Name,
Path: dir,
})
}
}
case *ast.TypeSpec:
// In the simpler case we will simply check whether the type
// declaration has the name we were looking for.
if x.Name.Name == typeID {
derivedName := deriveName(path, dir)
pluginTypes = append(pluginTypes, plugin{
Package: goPackage.Name,
PluginName: derivedName,
ImportName: deriveImport(x.Name.Name, derivedName),
TypeName: x.Name.Name,
Path: dir,
})
// The AST stops parsing when we return false. Once we
// find the symbol we want we can stop parsing.
return false
}
}
return true
})
}
}
return pluginTypes, nil
}
func discoverProviders() ([]plugin, error) {
path := "./builtin/providers"
typeID := "terraform.ResourceProvider"
typeName := "Provider"
return discoverTypesInPath(path, typeID, typeName)
}
func discoverProvisioners() ([]plugin, error) {
path := "./builtin/provisioners"
typeID := "ResourceProvisioner"
typeName := ""
return discoverTypesInPath(path, typeID, typeName)
}
const source = `// +build !core
//
// This file is automatically generated by scripts/generate-plugins.go -- Do not edit!
//
package command
import (
IMPORTS
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)
var InternalProviders = map[string]plugin.ProviderFunc{
PROVIDERS
}
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
PROVISIONERS
}
`

View File

@ -0,0 +1,102 @@
package main
import "testing"
func TestMakeProvisionerMap(t *testing.T) {
p := makeProvisionerMap([]plugin{
{
Package: "file",
PluginName: "file",
TypeName: "ResourceProvisioner",
Path: "builtin/provisioners/file",
ImportName: "fileresourceprovisioner",
},
{
Package: "localexec",
PluginName: "local-exec",
TypeName: "ResourceProvisioner",
Path: "builtin/provisioners/local-exec",
ImportName: "localexecresourceprovisioner",
},
{
Package: "remoteexec",
PluginName: "remote-exec",
TypeName: "ResourceProvisioner",
Path: "builtin/provisioners/remote-exec",
ImportName: "remoteexecresourceprovisioner",
},
})
expected := ` "file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) },
"local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) },
"remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) },
`
if p != expected {
t.Errorf("Provisioner output does not match expected format.\n -- Expected -- \n%s\n -- Found --\n%s\n", expected, p)
}
}
func TestDeriveName(t *testing.T) {
actual := deriveName("builtin/provisioners", "builtin/provisioners/magic/remote-exec")
expected := "magic-remote-exec"
if actual != expected {
t.Errorf("Expected %s; found %s", expected, actual)
}
}
func TestDeriveImport(t *testing.T) {
actual := deriveImport("provider", "magic-aws")
expected := "magicawsprovider"
if actual != expected {
t.Errorf("Expected %s; found %s", expected, actual)
}
}
func contains(plugins []plugin, name string) bool {
for _, plugin := range plugins {
if plugin.PluginName == name {
return true
}
}
return false
}
func TestDiscoverTypesProviders(t *testing.T) {
plugins, err := discoverTypesInPath("../builtin/providers", "terraform.ResourceProvider", "Provider")
if err != nil {
t.Fatalf(err.Error())
}
// We're just going to spot-check, not do this exhaustively
if !contains(plugins, "aws") {
t.Errorf("Expected to find aws provider")
}
if !contains(plugins, "docker") {
t.Errorf("Expected to find docker provider")
}
if !contains(plugins, "dnsimple") {
t.Errorf("Expected to find dnsimple provider")
}
if !contains(plugins, "triton") {
t.Errorf("Expected to find triton provider")
}
if contains(plugins, "file") {
t.Errorf("Found unexpected provider file")
}
}
func TestDiscoverTypesProvisioners(t *testing.T) {
plugins, err := discoverTypesInPath("../builtin/provisioners", "ResourceProvisioner", "")
if err != nil {
t.Fatalf(err.Error())
}
if !contains(plugins, "chef") {
t.Errorf("Expected to find chef provisioner")
}
if !contains(plugins, "remote-exec") {
t.Errorf("Expected to find remote-exec provisioner")
}
if contains(plugins, "aws") {
t.Errorf("Found unexpected provisioner aws")
}
}

View File

@ -159,7 +159,7 @@ func TestAtlasClient_UnresolvableConflict(t *testing.T) {
select {
case <-doneCh:
// OK
case <-time.After(50 * time.Millisecond):
case <-time.After(500 * time.Millisecond):
t.Fatalf("Timed out after 50ms, probably because retrying infinitely.")
}
}
@ -245,7 +245,7 @@ func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) {
// loads the state.
var testStateModuleOrderChange = []byte(
`{
"version": 1,
"version": 2,
"serial": 1,
"modules": [
{
@ -276,7 +276,7 @@ var testStateModuleOrderChange = []byte(
var testStateSimple = []byte(
`{
"version": 1,
"version": 2,
"serial": 1,
"modules": [
{

View File

@ -36,7 +36,7 @@ func TestState(t *testing.T, s interface{}) {
if ws, ok := s.(StateWriter); ok {
current.Modules = append(current.Modules, &terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"bar": "baz",
},
})
@ -94,7 +94,7 @@ func TestState(t *testing.T, s interface{}) {
current.Modules = []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root", "somewhere"},
Outputs: map[string]string{"serialCheck": "true"},
Outputs: map[string]interface{}{"serialCheck": "true"},
},
}
if err := writer.WriteState(current); err != nil {
@ -123,7 +123,7 @@ func TestStateInitial() *terraform.State {
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root", "child"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},

View File

@ -35,16 +35,17 @@ const (
// ContextOpts are the user-configurable options to create a context with
// NewContext.
type ContextOpts struct {
Destroy bool
Diff *Diff
Hooks []Hook
Module *module.Tree
Parallelism int
State *State
Providers map[string]ResourceProviderFactory
Provisioners map[string]ResourceProvisionerFactory
Targets []string
Variables map[string]string
Destroy bool
Diff *Diff
Hooks []Hook
Module *module.Tree
Parallelism int
State *State
StateFutureAllowed bool
Providers map[string]ResourceProviderFactory
Provisioners map[string]ResourceProvisionerFactory
Targets []string
Variables map[string]string
UIInput UIInput
}
@ -78,7 +79,7 @@ type Context struct {
// Once a Context is creator, the pointer values within ContextOpts
// should not be mutated in any way, since the pointers are copied, not
// the values themselves.
func NewContext(opts *ContextOpts) *Context {
func NewContext(opts *ContextOpts) (*Context, error) {
// Copy all the hooks and add our stop hook. We don't append directly
// to the Config so that we're not modifying that in-place.
sh := new(stopHook)
@ -92,6 +93,22 @@ func NewContext(opts *ContextOpts) *Context {
state.init()
}
// If our state is from the future, then error. Callers can avoid
// this error by explicitly setting `StateFutureAllowed`.
if !opts.StateFutureAllowed && state.FromFutureTerraform() {
return nil, fmt.Errorf(
"Terraform doesn't allow running any operations against a state\n"+
"that was written by a future Terraform version. The state is\n"+
"reporting it is written by Terraform '%s'.\n\n"+
"Please run at least that version of Terraform to continue.",
state.TFVersion)
}
// Explicitly reset our state version to our current version so that
// any operations we do will write out that our latest version
// has run.
state.TFVersion = Version
// Determine parallelism, default to 10. We do this both to limit
// CPU pressure but also to have an extra guard against rate throttling
// from providers.
@ -135,7 +152,7 @@ func NewContext(opts *ContextOpts) *Context {
parallelSem: NewSemaphore(par),
providerInputConfig: make(map[string]map[string]interface{}),
sh: sh,
}
}, nil
}
type ContextGraphOpts struct {
@ -208,6 +225,8 @@ func (c *Context) Input(mode InputMode) error {
continue
case config.VariableTypeMap:
continue
case config.VariableTypeList:
continue
case config.VariableTypeString:
// Good!
default:

View File

@ -48,6 +48,45 @@ func TestContext2Apply(t *testing.T) {
}
}
func TestContext2Apply_mapVarBetweenModules(t *testing.T) {
m := testModule(t, "apply-map-var-through-module")
p := testProvider("null")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"null": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`<no state>
Outputs:
amis_from_module = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }
module.test:
null_resource.noop:
ID = foo
Outputs:
amis_out = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }`)
if actual != expected {
t.Fatalf("expected: \n%s\n\ngot: \n%s\n", expected, actual)
}
}
func TestContext2Apply_providerAlias(t *testing.T) {
m := testModule(t, "apply-provider-alias")
p := testProvider("aws")
@ -969,7 +1008,7 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) {
},
},
},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"a_output": "a",
},
},
@ -1438,7 +1477,7 @@ func TestContext2Apply_outputOrphan(t *testing.T) {
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
@ -2559,11 +2598,14 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) {
t.Fatalf("err: %s", err)
}
ctx = planFromFile.Context(&ContextOpts{
ctx, err = planFromFile.Context(&ContextOpts{
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err = ctx.Apply()
if err != nil {
@ -3066,7 +3108,7 @@ func TestContext2Apply_outputInvalid(t *testing.T) {
if err == nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(err.Error(), "is not a string") {
if !strings.Contains(err.Error(), "is not a valid type") {
t.Fatalf("err: %s", err)
}
}
@ -3144,7 +3186,7 @@ func TestContext2Apply_outputList(t *testing.T) {
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputListStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
}
}
@ -3850,7 +3892,7 @@ func TestContext2Apply_vars(t *testing.T) {
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyVarsStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
t.Fatalf("expected: %s\n got:\n%s", expected, actual)
}
}
@ -4115,11 +4157,14 @@ func TestContext2Apply_issue5254(t *testing.T) {
t.Fatalf("err: %s", err)
}
ctx = planFromFile.Context(&ContextOpts{
ctx, err = planFromFile.Context(&ContextOpts{
Providers: map[string]ResourceProviderFactory{
"template": testProviderFuncFixed(p),
},
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err = ctx.Apply()
if err != nil {
@ -4189,12 +4234,15 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) {
t.Fatalf("err: %s", err)
}
ctx = planFromFile.Context(&ContextOpts{
ctx, err = planFromFile.Context(&ContextOpts{
Module: testModule(t, "apply-tainted-targets"),
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {

View File

@ -45,7 +45,7 @@ func TestContext2Input(t *testing.T) {
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformInputVarsStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual)
}
}

View File

@ -452,7 +452,7 @@ func TestContext2Refresh_output(t *testing.T) {
},
},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "foo",
},
},
@ -738,7 +738,7 @@ func TestContext2Refresh_orphanModule(t *testing.T) {
},
},
},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"id": "i-bcd234",
"grandchild_id": "i-cde345",
},
@ -752,7 +752,7 @@ func TestContext2Refresh_orphanModule(t *testing.T) {
},
},
},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"id": "i-cde345",
},
},

View File

@ -7,8 +7,71 @@ import (
"time"
)
func TestNewContextState(t *testing.T) {
cases := map[string]struct {
Input *ContextOpts
Err bool
}{
"empty TFVersion": {
&ContextOpts{
State: &State{},
},
false,
},
"past TFVersion": {
&ContextOpts{
State: &State{TFVersion: "0.1.2"},
},
false,
},
"equal TFVersion": {
&ContextOpts{
State: &State{TFVersion: Version},
},
false,
},
"future TFVersion": {
&ContextOpts{
State: &State{TFVersion: "99.99.99"},
},
true,
},
"future TFVersion, allowed": {
&ContextOpts{
State: &State{TFVersion: "99.99.99"},
StateFutureAllowed: true,
},
false,
},
}
for k, tc := range cases {
ctx, err := NewContext(tc.Input)
if (err != nil) != tc.Err {
t.Fatalf("%s: err: %s", k, err)
}
if err != nil {
continue
}
// Version should always be set to our current
if ctx.state.TFVersion != Version {
t.Fatalf("%s: state not set to current version", k)
}
}
}
func testContext2(t *testing.T, opts *ContextOpts) *Context {
return NewContext(opts)
ctx, err := NewContext(opts)
if err != nil {
t.Fatalf("err: %s", err)
}
return ctx
}
func testApplyFn(

View File

@ -68,7 +68,7 @@ type EvalContext interface {
// SetVariables sets the variables for the module within
// this context with the name n. This function call is additive:
// the second parameter is merged with any previous call.
SetVariables(string, map[string]string)
SetVariables(string, map[string]interface{})
// Diff returns the global diff as well as the lock that should
// be used to modify that diff.

View File

@ -23,7 +23,7 @@ type BuiltinEvalContext struct {
// as the Interpolater itself, it is protected by InterpolaterVarLock
// which must be locked during any access to the map.
Interpolater *Interpolater
InterpolaterVars map[string]map[string]string
InterpolaterVars map[string]map[string]interface{}
InterpolaterVarLock *sync.Mutex
Hooks []Hook
@ -311,7 +311,7 @@ func (ctx *BuiltinEvalContext) Path() []string {
return ctx.PathValue
}
func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]string) {
func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]interface{}) {
ctx.InterpolaterVarLock.Lock()
defer ctx.InterpolaterVarLock.Unlock()
@ -322,7 +322,7 @@ func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]string) {
vars := ctx.InterpolaterVars[key]
if vars == nil {
vars = make(map[string]string)
vars = make(map[string]interface{})
ctx.InterpolaterVars[key] = vars
}

View File

@ -74,7 +74,7 @@ type MockEvalContext struct {
SetVariablesCalled bool
SetVariablesModule string
SetVariablesVariables map[string]string
SetVariablesVariables map[string]interface{}
DiffCalled bool
DiffDiff *Diff
@ -183,7 +183,7 @@ func (c *MockEvalContext) Path() []string {
return c.PathPath
}
func (c *MockEvalContext) SetVariables(n string, vs map[string]string) {
func (c *MockEvalContext) SetVariables(n string, vs map[string]interface{}) {
c.SetVariablesCalled = true
c.SetVariablesModule = n
c.SetVariablesVariables = vs

View File

@ -2,6 +2,7 @@ package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/config"
)
@ -45,7 +46,8 @@ type EvalWriteOutput struct {
func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
cfg, err := ctx.Interpolate(n.Value, nil)
if err != nil {
// Ignore it
// Log error but continue anyway
log.Printf("[WARN] Output interpolation %q failed: %s", n.Name, err)
}
state, lock := ctx.State()
@ -76,16 +78,16 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
}
}
// If it is a list of values, get the first one
if list, ok := valueRaw.([]interface{}); ok {
valueRaw = list[0]
switch valueTyped := valueRaw.(type) {
case string:
mod.Outputs[n.Name] = valueTyped
case []interface{}:
mod.Outputs[n.Name] = valueTyped
case map[string]interface{}:
mod.Outputs[n.Name] = valueTyped
default:
return nil, fmt.Errorf("output %s is not a valid type (%T)\n", n.Name, valueTyped)
}
if _, ok := valueRaw.(string); !ok {
return nil, fmt.Errorf("output %s is not a string", n.Name)
}
// Write the output
mod.Outputs[n.Name] = valueRaw.(string)
return nil, nil
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/mitchellh/mapstructure"
@ -26,7 +25,7 @@ import (
// use of the values since it is only valid to pass string values. The
// structure is in place for extension of the type system, however.
type EvalTypeCheckVariable struct {
Variables map[string]string
Variables map[string]interface{}
ModulePath []string
ModuleTree *module.Tree
}
@ -43,29 +42,56 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
prototypes[variable.Name] = variable.Type()
}
// Only display a module in an error message if we are not in the root module
modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], "."))
if len(n.ModulePath) == 1 {
modulePathDescription = ""
}
for name, declaredType := range prototypes {
// This is only necessary when we _actually_ check. It is left as a reminder
// that at the current time we are dealing with a type system consisting only
// of strings and maps - where the only valid inter-module variable type is
// string.
_, ok := n.Variables[name]
proposedValue, ok := n.Variables[name]
if !ok {
// This means the default value should be used as no overriding value
// has been set. Therefore we should continue as no check is necessary.
continue
}
if proposedValue == config.UnknownVariableValue {
continue
}
switch declaredType {
case config.VariableTypeString:
// This will need actual verification once we aren't dealing with
// a map[string]string but this is sufficient for now.
continue
default:
// Only display a module if we are not in the root module
modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], "."))
if len(n.ModulePath) == 1 {
modulePathDescription = ""
switch proposedValue.(type) {
case string:
continue
default:
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
name, modulePathDescription, declaredType.Printable(), proposedValue)
}
case config.VariableTypeMap:
switch proposedValue.(type) {
case map[string]interface{}:
continue
default:
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
name, modulePathDescription, declaredType.Printable(), proposedValue)
}
case config.VariableTypeList:
switch proposedValue.(type) {
case []interface{}:
continue
default:
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
name, modulePathDescription, declaredType.Printable(), proposedValue)
}
default:
// This will need the actual type substituting when we have more than
// just strings and maps.
return nil, fmt.Errorf("variable %s%s should be type %s, got type string",
@ -80,7 +106,7 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
// explicitly for interpolation later.
type EvalSetVariables struct {
Module *string
Variables map[string]string
Variables map[string]interface{}
}
// TODO: test
@ -93,31 +119,43 @@ func (n *EvalSetVariables) Eval(ctx EvalContext) (interface{}, error) {
// given configuration, and uses the final values as a way to set the
// mapping.
type EvalVariableBlock struct {
Config **ResourceConfig
Variables map[string]string
Config **ResourceConfig
VariableValues map[string]interface{}
}
// TODO: test
func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) {
// Clear out the existing mapping
for k, _ := range n.Variables {
delete(n.Variables, k)
for k, _ := range n.VariableValues {
delete(n.VariableValues, k)
}
// Get our configuration
rc := *n.Config
for k, v := range rc.Config {
var vStr string
if err := mapstructure.WeakDecode(v, &vStr); err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf(
"%s: error reading value: {{err}}", k), err)
var vString string
if err := mapstructure.WeakDecode(v, &vString); err == nil {
n.VariableValues[k] = vString
continue
}
n.Variables[k] = vStr
var vMap map[string]interface{}
if err := mapstructure.WeakDecode(v, &vMap); err == nil {
n.VariableValues[k] = vMap
continue
}
var vSlice []interface{}
if err := mapstructure.WeakDecode(v, &vSlice); err == nil {
n.VariableValues[k] = vSlice
continue
}
return nil, fmt.Errorf("Variable value for %s is not a string, list or map type", k)
}
for k, _ := range rc.Raw {
if _, ok := n.Variables[k]; !ok {
n.Variables[k] = config.UnknownVariableValue
if _, ok := n.VariableValues[k]; !ok {
n.VariableValues[k] = config.UnknownVariableValue
}
}

View File

@ -69,7 +69,7 @@ func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error
return &graphNodeModuleExpanded{
Original: n,
Graph: graph,
Variables: make(map[string]string),
Variables: make(map[string]interface{}),
}, nil
}
@ -107,7 +107,7 @@ type graphNodeModuleExpanded struct {
// Variables is a map of the input variables. This reference should
// be shared with ModuleInputTransformer in order to create a connection
// where the variables are set properly.
Variables map[string]string
Variables map[string]interface{}
}
func (n *graphNodeModuleExpanded) Name() string {
@ -147,8 +147,8 @@ func (n *graphNodeModuleExpanded) EvalTree() EvalNode {
},
&EvalVariableBlock{
Config: &resourceConfig,
Variables: n.Variables,
Config: &resourceConfig,
VariableValues: n.Variables,
},
},
}

View File

@ -114,7 +114,7 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode {
// Otherwise, interpolate the value of this variable and set it
// within the variables mapping.
var config *ResourceConfig
variables := make(map[string]string)
variables := make(map[string]interface{})
return &EvalSequence{
Nodes: []EvalNode{
&EvalInterpolate{
@ -123,8 +123,8 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode {
},
&EvalVariableBlock{
Config: &config,
Variables: variables,
Config: &config,
VariableValues: variables,
},
&EvalTypeCheckVariable{

View File

@ -27,7 +27,7 @@ type ContextGraphWalker struct {
once sync.Once
contexts map[string]*BuiltinEvalContext
contextLock sync.Mutex
interpolaterVars map[string]map[string]string
interpolaterVars map[string]map[string]interface{}
interpolaterVarLock sync.Mutex
providerCache map[string]ResourceProvider
providerConfigCache map[string]*ResourceConfig
@ -49,7 +49,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
}
// Setup the variables for this interpolater
variables := make(map[string]string)
variables := make(map[string]interface{})
if len(path) <= 1 {
for k, v := range w.Context.variables {
variables[k] = v
@ -81,12 +81,12 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
StateValue: w.Context.state,
StateLock: &w.Context.stateLock,
Interpolater: &Interpolater{
Operation: w.Operation,
Module: w.Context.module,
State: w.Context.state,
StateLock: &w.Context.stateLock,
Variables: variables,
VariablesLock: &w.interpolaterVarLock,
Operation: w.Operation,
Module: w.Context.module,
State: w.Context.state,
StateLock: &w.Context.stateLock,
VariableValues: variables,
VariableValuesLock: &w.interpolaterVarLock,
},
InterpolaterVars: w.interpolaterVars,
InterpolaterVarLock: &w.interpolaterVarLock,
@ -150,5 +150,5 @@ func (w *ContextGraphWalker) init() {
w.providerCache = make(map[string]ResourceProvider, 5)
w.providerConfigCache = make(map[string]*ResourceConfig, 5)
w.provisionerCache = make(map[string]ResourceProvisioner, 5)
w.interpolaterVars = make(map[string]map[string]string, 5)
w.interpolaterVars = make(map[string]map[string]interface{}, 5)
}

View File

@ -9,6 +9,7 @@ import (
"strings"
"sync"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
@ -23,12 +24,12 @@ const (
// Interpolater is the structure responsible for determining the values
// for interpolations such as `aws_instance.foo.bar`.
type Interpolater struct {
Operation walkOperation
Module *module.Tree
State *State
StateLock *sync.RWMutex
Variables map[string]string
VariablesLock *sync.Mutex
Operation walkOperation
Module *module.Tree
State *State
StateLock *sync.RWMutex
VariableValues map[string]interface{}
VariableValuesLock *sync.Mutex
}
// InterpolationScope is the current scope of execution. This is required
@ -52,12 +53,18 @@ func (i *Interpolater) Values(
mod = i.Module.Child(scope.Path[1:])
}
for _, v := range mod.Config().Variables {
for k, val := range v.DefaultsMap() {
result[k] = ast.Variable{
Value: val,
Type: ast.TypeString,
}
// Set default variables
if v.Default == nil {
continue
}
n := fmt.Sprintf("var.%s", v.Name)
variable, err := hil.InterfaceToVariable(v.Default)
if err != nil {
return nil, fmt.Errorf("invalid default map value for %s: %v", v.Name, v.Default)
}
result[n] = variable
}
}
@ -110,6 +117,13 @@ func (i *Interpolater) valueCountVar(
}
}
func unknownVariable() ast.Variable {
return ast.Variable{
Type: ast.TypeString,
Value: config.UnknownVariableValue,
}
}
func (i *Interpolater) valueModuleVar(
scope *InterpolationScope,
n string,
@ -136,7 +150,6 @@ func (i *Interpolater) valueModuleVar(
defer i.StateLock.RUnlock()
// Get the module where we're looking for the value
var value string
mod := i.State.ModuleByPath(path)
if mod == nil {
// If the module doesn't exist, then we can return an empty string.
@ -145,21 +158,22 @@ func (i *Interpolater) valueModuleVar(
// modules reference other modules, and graph ordering should
// ensure that the module is in the state, so if we reach this
// point otherwise it really is a panic.
value = config.UnknownVariableValue
result[n] = unknownVariable()
} else {
// Get the value from the outputs
var ok bool
value, ok = mod.Outputs[v.Field]
if !ok {
if value, ok := mod.Outputs[v.Field]; ok {
output, err := hil.InterfaceToVariable(value)
if err != nil {
return err
}
result[n] = output
} else {
// Same reasons as the comment above.
value = config.UnknownVariableValue
result[n] = unknownVariable()
}
}
result[n] = ast.Variable{
Value: value,
Type: ast.TypeString,
}
return nil
}
@ -216,21 +230,26 @@ func (i *Interpolater) valueResourceVar(
return nil
}
var attr string
var err error
if v.Multi && v.Index == -1 {
attr, err = i.computeResourceMultiVariable(scope, v)
variable, err := i.computeResourceMultiVariable(scope, v)
if err != nil {
return err
}
if variable == nil {
return fmt.Errorf("no error reported by variable %q is nil", v.Name)
}
result[n] = *variable
} else {
attr, err = i.computeResourceVariable(scope, v)
}
if err != nil {
return err
variable, err := i.computeResourceVariable(scope, v)
if err != nil {
return err
}
if variable == nil {
return fmt.Errorf("no error reported by variable %q is nil", v.Name)
}
result[n] = *variable
}
result[n] = ast.Variable{
Value: attr,
Type: ast.TypeString,
}
return nil
}
@ -274,33 +293,44 @@ func (i *Interpolater) valueUserVar(
n string,
v *config.UserVariable,
result map[string]ast.Variable) error {
i.VariablesLock.Lock()
defer i.VariablesLock.Unlock()
val, ok := i.Variables[v.Name]
i.VariableValuesLock.Lock()
defer i.VariableValuesLock.Unlock()
val, ok := i.VariableValues[v.Name]
if ok {
result[n] = ast.Variable{
Value: val,
Type: ast.TypeString,
varValue, err := hil.InterfaceToVariable(val)
if err != nil {
return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s",
v.Name, val, err)
}
result[n] = varValue
return nil
}
if _, ok := result[n]; !ok && i.Operation == walkValidate {
result[n] = ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
result[n] = unknownVariable()
return nil
}
// Look up if we have any variables with this prefix because
// those are map overrides. Include those.
for k, val := range i.Variables {
for k, val := range i.VariableValues {
if strings.HasPrefix(k, v.Name+".") {
result["var."+k] = ast.Variable{
Value: val,
Type: ast.TypeString,
keyComponents := strings.Split(k, ".")
overrideKey := keyComponents[len(keyComponents)-1]
mapInterface, ok := result["var."+v.Name]
if !ok {
return fmt.Errorf("override for non-existent variable: %s", v.Name)
}
mapVariable := mapInterface.Value.(map[string]ast.Variable)
varValue, err := hil.InterfaceToVariable(val)
if err != nil {
return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s",
v.Name, val, err)
}
mapVariable[overrideKey] = varValue
}
}
@ -309,7 +339,7 @@ func (i *Interpolater) valueUserVar(
func (i *Interpolater) computeResourceVariable(
scope *InterpolationScope,
v *config.ResourceVariable) (string, error) {
v *config.ResourceVariable) (*ast.Variable, error) {
id := v.ResourceId()
if v.Multi {
id = fmt.Sprintf("%s.%d", id, v.Index)
@ -318,16 +348,18 @@ func (i *Interpolater) computeResourceVariable(
i.StateLock.RLock()
defer i.StateLock.RUnlock()
unknownVariable := unknownVariable()
// Get the information about this resource variable, and verify
// that it exists and such.
module, _, err := i.resourceVariableInfo(scope, v)
if err != nil {
return "", err
return nil, err
}
// If we have no module in the state yet or count, return empty
if module == nil || len(module.Resources) == 0 {
return "", nil
return nil, nil
}
// Get the resource out from the state. We know the state exists
@ -349,12 +381,13 @@ func (i *Interpolater) computeResourceVariable(
}
if attr, ok := r.Primary.Attributes[v.Field]; ok {
return attr, nil
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
}
// computed list attribute
// computed list or map attribute
if _, ok := r.Primary.Attributes[v.Field+".#"]; ok {
return i.interpolateListAttribute(v.Field, r.Primary.Attributes)
variable, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes)
return &variable, err
}
// At apply time, we can't do the "maybe has it" check below
@ -377,13 +410,13 @@ func (i *Interpolater) computeResourceVariable(
// Lists and sets make this
key := fmt.Sprintf("%s.#", strings.Join(parts[:i], "."))
if attr, ok := r.Primary.Attributes[key]; ok {
return attr, nil
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
}
// Maps make this
key = fmt.Sprintf("%s", strings.Join(parts[:i], "."))
if attr, ok := r.Primary.Attributes[key]; ok {
return attr, nil
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
}
}
}
@ -393,7 +426,7 @@ MISSING:
// semantic level. If we reached this point and don't have variables,
// just return the computed value.
if scope == nil && scope.Resource == nil {
return config.UnknownVariableValue, nil
return &unknownVariable, nil
}
// If the operation is refresh, it isn't an error for a value to
@ -407,10 +440,10 @@ MISSING:
// For an input walk, computed values are okay to return because we're only
// looking for missing variables to prompt the user for.
if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput {
return config.UnknownVariableValue, nil
return &unknownVariable, nil
}
return "", fmt.Errorf(
return nil, fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+
"for variable '%s'",
id,
@ -420,21 +453,23 @@ MISSING:
func (i *Interpolater) computeResourceMultiVariable(
scope *InterpolationScope,
v *config.ResourceVariable) (string, error) {
v *config.ResourceVariable) (*ast.Variable, error) {
i.StateLock.RLock()
defer i.StateLock.RUnlock()
unknownVariable := unknownVariable()
// Get the information about this resource variable, and verify
// that it exists and such.
module, cr, err := i.resourceVariableInfo(scope, v)
if err != nil {
return "", err
return nil, err
}
// Get the count so we know how many to iterate over
count, err := cr.Count()
if err != nil {
return "", fmt.Errorf(
return nil, fmt.Errorf(
"Error reading %s count: %s",
v.ResourceId(),
err)
@ -442,7 +477,7 @@ func (i *Interpolater) computeResourceMultiVariable(
// If we have no module in the state yet or count, return empty
if module == nil || len(module.Resources) == 0 || count == 0 {
return "", nil
return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
}
var values []string
@ -464,32 +499,37 @@ func (i *Interpolater) computeResourceMultiVariable(
continue
}
attr, ok := r.Primary.Attributes[v.Field]
if !ok {
// computed list attribute
_, ok := r.Primary.Attributes[v.Field+".#"]
if !ok {
continue
if singleAttr, ok := r.Primary.Attributes[v.Field]; ok {
if singleAttr == config.UnknownVariableValue {
return &unknownVariable, nil
}
attr, err = i.interpolateListAttribute(v.Field, r.Primary.Attributes)
if err != nil {
return "", err
}
}
if config.IsStringList(attr) {
for _, s := range config.StringList(attr).Slice() {
values = append(values, s)
}
values = append(values, singleAttr)
continue
}
// If any value is unknown, the whole thing is unknown
if attr == config.UnknownVariableValue {
return config.UnknownVariableValue, nil
// computed list attribute
_, ok = r.Primary.Attributes[v.Field+".#"]
if !ok {
continue
}
multiAttr, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes)
if err != nil {
return nil, err
}
values = append(values, attr)
if multiAttr == unknownVariable {
return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
}
for _, element := range multiAttr.Value.([]ast.Variable) {
strVal := element.Value.(string)
if strVal == config.UnknownVariableValue {
return &unknownVariable, nil
}
values = append(values, strVal)
}
}
if len(values) == 0 {
@ -504,10 +544,10 @@ func (i *Interpolater) computeResourceMultiVariable(
// For an input walk, computed values are okay to return because we're only
// looking for missing variables to prompt the user for.
if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput {
return config.UnknownVariableValue, nil
return &unknownVariable, nil
}
return "", fmt.Errorf(
return nil, fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+
"for variable '%s'",
v.ResourceId(),
@ -515,15 +555,16 @@ func (i *Interpolater) computeResourceMultiVariable(
v.FullKey())
}
return config.NewStringList(values).String(), nil
variable, err := hil.InterfaceToVariable(values)
return &variable, err
}
func (i *Interpolater) interpolateListAttribute(
func (i *Interpolater) interpolateComplexTypeAttribute(
resourceID string,
attributes map[string]string) (string, error) {
attributes map[string]string) (ast.Variable, error) {
attr := attributes[resourceID+".#"]
log.Printf("[DEBUG] Interpolating computed list attribute %s (%s)",
log.Printf("[DEBUG] Interpolating computed complex type attribute %s (%s)",
resourceID, attr)
// In Terraform's internal dotted representation of list-like attributes, the
@ -531,21 +572,40 @@ func (i *Interpolater) interpolateListAttribute(
// unknown". We must honor that meaning here so computed references can be
// treated properly during the plan phase.
if attr == config.UnknownVariableValue {
return attr, nil
return unknownVariable(), nil
}
// Otherwise we gather the values from the list-like attribute and return
// them.
var members []string
numberedListMember := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$")
for id, value := range attributes {
if numberedListMember.MatchString(id) {
members = append(members, value)
// At this stage we don't know whether the item is a list or a map, so we
// examine the keys to see whether they are all numeric.
var numericKeys []string
var allKeys []string
numberedListKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$")
otherListKey := regexp.MustCompile("^" + resourceID + "\\.([^#]+)$")
for id, _ := range attributes {
if numberedListKey.MatchString(id) {
numericKeys = append(numericKeys, id)
}
if submatches := otherListKey.FindAllStringSubmatch(id, -1); len(submatches) > 0 {
allKeys = append(allKeys, submatches[0][1])
}
}
sort.Strings(members)
return config.NewStringList(members).String(), nil
if len(numericKeys) == len(allKeys) {
// This is a list
var members []string
for _, key := range numericKeys {
members = append(members, attributes[key])
}
sort.Strings(members)
return hil.InterfaceToVariable(members)
} else {
// This is a map
members := make(map[string]interface{})
for _, key := range allKeys {
members[key] = attributes[resourceID+"."+key]
}
return hil.InterfaceToVariable(members)
}
}
func (i *Interpolater) resourceVariableInfo(

View File

@ -7,6 +7,7 @@ import (
"sync"
"testing"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
)
@ -67,7 +68,7 @@ func TestInterpolater_moduleVariable(t *testing.T) {
},
&ModuleState{
Path: []string{RootModuleName, "child"},
Outputs: map[string]string{
Outputs: map[string]interface{}{
"foo": "bar",
},
},
@ -210,6 +211,11 @@ func TestInterpolater_resourceVariableMulti(t *testing.T) {
})
}
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
func TestInterpolator_resourceMultiAttributes(t *testing.T) {
lock := new(sync.RWMutex)
state := &State{
@ -251,31 +257,24 @@ func TestInterpolator_resourceMultiAttributes(t *testing.T) {
Path: rootModulePath,
}
name_servers := []string{
name_servers := []interface{}{
"ns-1334.awsdns-38.org",
"ns-1680.awsdns-18.co.uk",
"ns-498.awsdns-62.com",
"ns-601.awsdns-11.net",
}
expectedNameServers := config.NewStringList(name_servers).String()
// More than 1 element
testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers", ast.Variable{
Value: expectedNameServers,
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers",
interfaceToVariableSwallowError(name_servers))
// Exactly 1 element
testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners", ast.Variable{
Value: config.NewStringList([]string{"red"}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners",
interfaceToVariableSwallowError([]interface{}{"red"}))
// Zero elements
testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing", ast.Variable{
Value: config.NewStringList([]string{}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing",
interfaceToVariableSwallowError([]interface{}{}))
// Maps still need to work
testInterpolate(t, i, scope, "aws_route53_zone.yada.tags.Name", ast.Variable{
@ -290,7 +289,7 @@ func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) {
Path: rootModulePath,
}
name_servers := []string{
name_servers := []interface{}{
"ns-1334.awsdns-38.org",
"ns-1680.awsdns-18.co.uk",
"ns-498.awsdns-62.com",
@ -302,50 +301,38 @@ func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) {
}
// More than 1 element
expectedNameServers := config.NewStringList(name_servers[0:4]).String()
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers", ast.Variable{
Value: expectedNameServers,
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers",
interfaceToVariableSwallowError(name_servers[0:4]))
// More than 1 element in both
expectedNameServers = config.NewStringList(name_servers).String()
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers", ast.Variable{
Value: expectedNameServers,
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers",
interfaceToVariableSwallowError(name_servers))
// Exactly 1 element
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners", ast.Variable{
Value: config.NewStringList([]string{"red"}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners",
interfaceToVariableSwallowError([]interface{}{"red"}))
// Exactly 1 element in both
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners", ast.Variable{
Value: config.NewStringList([]string{"red", "blue"}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners",
interfaceToVariableSwallowError([]interface{}{"red", "blue"}))
// Zero elements
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing", ast.Variable{
Value: config.NewStringList([]string{}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing",
interfaceToVariableSwallowError([]interface{}{}))
// Zero + 1 element
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special", ast.Variable{
Value: config.NewStringList([]string{"extra"}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special",
interfaceToVariableSwallowError([]interface{}{"extra"}))
// Maps still need to work
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.tags.Name", ast.Variable{
Value: "reindeer",
Type: ast.TypeString,
})
// Maps still need to work in both
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name", ast.Variable{
Value: config.NewStringList([]string{"reindeer", "white-hart"}).String(),
Type: ast.TypeString,
})
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name",
interfaceToVariableSwallowError([]interface{}{"reindeer", "white-hart"}))
}
func TestInterpolator_resourceMultiAttributesComputed(t *testing.T) {

View File

@ -34,7 +34,7 @@ type Plan struct {
//
// The following fields in opts are overridden by the plan: Config,
// Diff, State, Variables.
func (p *Plan) Context(opts *ContextOpts) *Context {
func (p *Plan) Context(opts *ContextOpts) (*Context, error) {
opts.Diff = p.Diff
opts.Module = p.Module
opts.State = p.State

View File

@ -18,9 +18,10 @@ type ResourceAddress struct {
// Addresses a specific resource that occurs in a list
Index int
InstanceType InstanceType
Name string
Type string
InstanceType InstanceType
InstanceTypeSet bool
Name string
Type string
}
// Copy returns a copy of this ResourceAddress
@ -38,6 +39,35 @@ func (r *ResourceAddress) Copy() *ResourceAddress {
return n
}
// String outputs the address that parses into this address.
func (r *ResourceAddress) String() string {
var result []string
for _, p := range r.Path {
result = append(result, "module", p)
}
if r.Type != "" {
result = append(result, r.Type)
}
if r.Name != "" {
name := r.Name
switch r.InstanceType {
case TypeDeposed:
name += ".deposed"
case TypeTainted:
name += ".tainted"
}
if r.Index >= 0 {
name += fmt.Sprintf("[%d]", r.Index)
}
result = append(result, name)
}
return strings.Join(result, ".")
}
func ParseResourceAddress(s string) (*ResourceAddress, error) {
matches, err := tokenizeResourceAddress(s)
if err != nil {
@ -54,11 +84,12 @@ func ParseResourceAddress(s string) (*ResourceAddress, error) {
path := ParseResourcePath(matches["path"])
return &ResourceAddress{
Path: path,
Index: resourceIndex,
InstanceType: instanceType,
Name: matches["name"],
Type: matches["type"],
Path: path,
Index: resourceIndex,
InstanceType: instanceType,
InstanceTypeSet: matches["instance_type"] != "",
Name: matches["name"],
Type: matches["type"],
}, nil
}

View File

@ -9,109 +9,124 @@ func TestParseResourceAddress(t *testing.T) {
cases := map[string]struct {
Input string
Expected *ResourceAddress
Output string
}{
"implicit primary, no specific index": {
Input: "aws_instance.foo",
Expected: &ResourceAddress{
"aws_instance.foo",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"implicit primary, explicit index": {
Input: "aws_instance.foo[2]",
Expected: &ResourceAddress{
"aws_instance.foo[2]",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 2,
},
"",
},
"implicit primary, explicit index over ten": {
Input: "aws_instance.foo[12]",
Expected: &ResourceAddress{
"aws_instance.foo[12]",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 12,
},
"",
},
"explicit primary, explicit index": {
Input: "aws_instance.foo.primary[2]",
Expected: &ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: 2,
"aws_instance.foo.primary[2]",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
InstanceTypeSet: true,
Index: 2,
},
"aws_instance.foo[2]",
},
"tainted": {
Input: "aws_instance.foo.tainted",
Expected: &ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypeTainted,
Index: -1,
"aws_instance.foo.tainted",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypeTainted,
InstanceTypeSet: true,
Index: -1,
},
"",
},
"deposed": {
Input: "aws_instance.foo.deposed",
Expected: &ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypeDeposed,
Index: -1,
"aws_instance.foo.deposed",
&ResourceAddress{
Type: "aws_instance",
Name: "foo",
InstanceType: TypeDeposed,
InstanceTypeSet: true,
Index: -1,
},
"",
},
"with a hyphen": {
Input: "aws_instance.foo-bar",
Expected: &ResourceAddress{
"aws_instance.foo-bar",
&ResourceAddress{
Type: "aws_instance",
Name: "foo-bar",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"in a module": {
Input: "module.child.aws_instance.foo",
Expected: &ResourceAddress{
"module.child.aws_instance.foo",
&ResourceAddress{
Path: []string{"child"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"nested modules": {
Input: "module.a.module.b.module.forever.aws_instance.foo",
Expected: &ResourceAddress{
"module.a.module.b.module.forever.aws_instance.foo",
&ResourceAddress{
Path: []string{"a", "b", "forever"},
Type: "aws_instance",
Name: "foo",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"just a module": {
Input: "module.a",
Expected: &ResourceAddress{
"module.a",
&ResourceAddress{
Path: []string{"a"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
"just a nested module": {
Input: "module.a.module.b",
Expected: &ResourceAddress{
"module.a.module.b",
&ResourceAddress{
Path: []string{"a", "b"},
Type: "",
Name: "",
InstanceType: TypePrimary,
Index: -1,
},
"",
},
}
@ -124,6 +139,14 @@ func TestParseResourceAddress(t *testing.T) {
if !reflect.DeepEqual(out, tc.Expected) {
t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out)
}
expected := tc.Input
if tc.Output != "" {
expected = tc.Output
}
if out.String() != expected {
t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, expected, out)
}
}
}

View File

@ -12,12 +12,13 @@ import (
"strconv"
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/config"
)
const (
// StateVersion is the current version for our state file
StateVersion = 1
StateVersion = 2
)
// rootModulePath is the path of the root module
@ -30,6 +31,9 @@ type State struct {
// Version is the protocol version. Currently only "1".
Version int `json:"version"`
// TFVersion is the version of Terraform that wrote this state.
TFVersion string `json:"terraform_version,omitempty"`
// Serial is incremented on any operation that modifies
// the State file. It is used to detect potentially conflicting
// updates.
@ -198,6 +202,122 @@ func (s *State) IsRemote() bool {
return true
}
// Remove removes the item in the state at the given address, returning
// any errors that may have occurred.
//
// If the address references a module state or resource, it will delete
// all children as well. To check what will be deleted, use a StateFilter
// first.
func (s *State) Remove(addr ...string) error {
// Filter out what we need to delete
filter := &StateFilter{State: s}
results, err := filter.Filter(addr...)
if err != nil {
return err
}
// If we have no results, just exit early, we're not going to do anything.
// While what happens below is fairly fast, this is an important early
// exit since the prune below might modify the state more and we don't
// want to modify the state if we don't have to.
if len(results) == 0 {
return nil
}
// Go through each result and grab what we need
removed := make(map[interface{}]struct{})
for _, r := range results {
// Convert the path to our own type
path := append([]string{"root"}, r.Path...)
// If we removed this already, then ignore
if _, ok := removed[r.Value]; ok {
continue
}
// If we removed the parent already, then ignore
if r.Parent != nil {
if _, ok := removed[r.Parent.Value]; ok {
continue
}
}
// Add this to the removed list
removed[r.Value] = struct{}{}
switch v := r.Value.(type) {
case *ModuleState:
s.removeModule(path, v)
case *ResourceState:
s.removeResource(path, v)
case *InstanceState:
s.removeInstance(path, r.Parent.Value.(*ResourceState), v)
default:
return fmt.Errorf("unknown type to delete: %T", r.Value)
}
}
// Prune since the removal functions often do the bare minimum to
// remove a thing and may leave around dangling empty modules, resources,
// etc. Prune will clean that all up.
s.prune()
return nil
}
func (s *State) removeModule(path []string, v *ModuleState) {
for i, m := range s.Modules {
if m == v {
s.Modules, s.Modules[len(s.Modules)-1] = append(s.Modules[:i], s.Modules[i+1:]...), nil
return
}
}
}
func (s *State) removeResource(path []string, v *ResourceState) {
// Get the module this resource lives in. If it doesn't exist, we're done.
mod := s.ModuleByPath(path)
if mod == nil {
return
}
// Find this resource. This is a O(N) lookup when if we had the key
// it could be O(1) but even with thousands of resources this shouldn't
// matter right now. We can easily up performance here when the time comes.
for k, r := range mod.Resources {
if r == v {
// Found it
delete(mod.Resources, k)
return
}
}
}
func (s *State) removeInstance(path []string, r *ResourceState, v *InstanceState) {
// Go through the resource and find the instance that matches this
// (if any) and remove it.
// Check primary
if r.Primary == v {
r.Primary = nil
return
}
// Check lists
lists := [][]*InstanceState{r.Tainted, r.Deposed}
for _, is := range lists {
for i, instance := range is {
if instance == v {
// Found it, remove it
is, is[len(is)-1] = append(is[:i], is[i+1:]...), nil
// Done
return
}
}
}
}
// RootModule returns the ModuleState for the root module
func (s *State) RootModule() *ModuleState {
root := s.ModuleByPath(rootModulePath)
@ -246,9 +366,10 @@ func (s *State) DeepCopy() *State {
return nil
}
n := &State{
Version: s.Version,
Serial: s.Serial,
Modules: make([]*ModuleState, 0, len(s.Modules)),
Version: s.Version,
TFVersion: s.TFVersion,
Serial: s.Serial,
Modules: make([]*ModuleState, 0, len(s.Modules)),
}
for _, mod := range s.Modules {
n.Modules = append(n.Modules, mod.deepcopy())
@ -271,7 +392,7 @@ func (s *State) IncrementSerialMaybe(other *State) {
if s.Serial > other.Serial {
return
}
if !s.Equal(other) {
if other.TFVersion != s.TFVersion || !s.Equal(other) {
if other.Serial > s.Serial {
s.Serial = other.Serial
}
@ -280,6 +401,18 @@ func (s *State) IncrementSerialMaybe(other *State) {
}
}
// FromFutureTerraform checks if this state was written by a Terraform
// version from the future.
func (s *State) FromFutureTerraform() bool {
// No TF version means it is certainly from the past
if s.TFVersion == "" {
return false
}
v := version.Must(version.NewVersion(s.TFVersion))
return SemVersion.LessThan(v)
}
func (s *State) init() {
if s.Version == 0 {
s.Version = StateVersion
@ -407,7 +540,7 @@ type ModuleState struct {
// Outputs declared by the module and maintained for each module
// even though only the root module technically needs to be kept.
// This allows operators to inspect values at the boundaries.
Outputs map[string]string `json:"outputs"`
Outputs map[string]interface{} `json:"outputs"`
// Resources is a mapping of the logically named resource to
// the state of the resource. Each resource may actually have
@ -442,7 +575,7 @@ func (m *ModuleState) Equal(other *ModuleState) bool {
return false
}
for k, v := range m.Outputs {
if other.Outputs[k] != v {
if !reflect.DeepEqual(other.Outputs[k], v) {
return false
}
}
@ -532,7 +665,7 @@ func (m *ModuleState) View(id string) *ModuleState {
func (m *ModuleState) init() {
if m.Outputs == nil {
m.Outputs = make(map[string]string)
m.Outputs = make(map[string]interface{})
}
if m.Resources == nil {
m.Resources = make(map[string]*ResourceState)
@ -545,7 +678,7 @@ func (m *ModuleState) deepcopy() *ModuleState {
}
n := &ModuleState{
Path: make([]string, len(m.Path)),
Outputs: make(map[string]string, len(m.Outputs)),
Outputs: make(map[string]interface{}, len(m.Outputs)),
Resources: make(map[string]*ResourceState, len(m.Resources)),
}
copy(n.Path, m.Path)
@ -670,7 +803,27 @@ func (m *ModuleState) String() string {
for _, k := range ks {
v := m.Outputs[k]
buf.WriteString(fmt.Sprintf("%s = %s\n", k, v))
switch vTyped := v.(type) {
case string:
buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
case []interface{}:
buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
case map[string]interface{}:
var mapKeys []string
for key, _ := range vTyped {
mapKeys = append(mapKeys, key)
}
sort.Strings(mapKeys)
var mapBuf bytes.Buffer
mapBuf.WriteString("{")
for _, key := range mapKeys {
mapBuf.WriteString(fmt.Sprintf("%s:%s ", key, vTyped[key]))
}
mapBuf.WriteString("}")
buf.WriteString(fmt.Sprintf("%s = %s\n", k, mapBuf.String()))
}
}
}
@ -1191,21 +1344,22 @@ func (e *EphemeralState) deepcopy() *EphemeralState {
func ReadState(src io.Reader) (*State, error) {
buf := bufio.NewReader(src)
// Check if this is a V1 format
// Check if this is a V0 format
start, err := buf.Peek(len(stateFormatMagic))
if err != nil {
return nil, fmt.Errorf("Failed to check for magic bytes: %v", err)
}
if string(start) == stateFormatMagic {
// Read the old state
old, err := ReadStateV1(buf)
old, err := ReadStateV0(buf)
if err != nil {
return nil, err
}
return upgradeV1State(old)
return upgradeV0State(old)
}
// Otherwise, must be V2
// Otherwise, must be V2 or V3 - V2 reads as V3 however so we need take
// no special action here - new state will be written as V3.
dec := json.NewDecoder(buf)
state := &State{}
if err := dec.Decode(state); err != nil {
@ -1219,6 +1373,19 @@ func ReadState(src io.Reader) (*State, error) {
state.Version)
}
// Make sure the version is semantic
if state.TFVersion != "" {
if _, err := version.NewVersion(state.TFVersion); err != nil {
return nil, fmt.Errorf(
"State contains invalid version: %s\n\n"+
"Terraform validates the version format prior to writing it. This\n"+
"means that this is invalid of the state becoming corrupted through\n"+
"some external means. Please manually modify the Terraform version\n"+
"field to be a proper semantic version.",
state.TFVersion)
}
}
// Sort it
state.sort()
@ -1233,6 +1400,19 @@ func WriteState(d *State, dst io.Writer) error {
// Ensure the version is set
d.Version = StateVersion
// If the TFVersion is set, verify it. We used to just set the version
// here, but this isn't safe since it changes the MD5 sum on some remote
// state storage backends such as Atlas. We now leave it be if needed.
if d.TFVersion != "" {
if _, err := version.NewVersion(d.TFVersion); err != nil {
return fmt.Errorf(
"Error writing state, invalid version: %s\n\n"+
"The Terraform version when writing the state must be a semantic\n"+
"version.",
d.TFVersion)
}
}
// Encode the data in a human-friendly way
data, err := json.MarshalIndent(d, "", " ")
if err != nil {
@ -1250,9 +1430,9 @@ func WriteState(d *State, dst io.Writer) error {
return nil
}
// upgradeV1State is used to upgrade a V1 state representation
// upgradeV0State is used to upgrade a V0 state representation
// into a proper State representation.
func upgradeV1State(old *StateV1) (*State, error) {
func upgradeV0State(old *StateV0) (*State, error) {
s := &State{}
s.init()
@ -1260,8 +1440,12 @@ func upgradeV1State(old *StateV1) (*State, error) {
// directly into the root module.
root := s.RootModule()
// Copy the outputs
root.Outputs = old.Outputs
// Copy the outputs, first converting them to map[string]interface{}
oldOutputs := make(map[string]interface{}, len(old.Outputs))
for key, value := range old.Outputs {
oldOutputs[key] = value
}
root.Outputs = oldOutputs
// Upgrade the resources
for id, rs := range old.Resources {

261
terraform/state_filter.go Normal file
View File

@ -0,0 +1,261 @@
package terraform
import (
"fmt"
"sort"
)
// StateFilter is responsible for filtering and searching a state.
//
// This is a separate struct from State rather than a method on State
// because StateFilter might create sidecar data structures to optimize
// filtering on the state.
//
// If you change the State, the filter created is invalid and either
// Reset should be called or a new one should be allocated. StateFilter
// will not watch State for changes and do this for you. If you filter after
// changing the State without calling Reset, the behavior is not defined.
type StateFilter struct {
State *State
}
// Filter takes the addresses specified by fs and finds all the matches.
// The values of fs are resource addressing syntax that can be parsed by
// ParseResourceAddress.
func (f *StateFilter) Filter(fs ...string) ([]*StateFilterResult, error) {
// Parse all the addresses
as := make([]*ResourceAddress, len(fs))
for i, v := range fs {
a, err := ParseResourceAddress(v)
if err != nil {
return nil, fmt.Errorf("Error parsing address '%s': %s", v, err)
}
as[i] = a
}
// If we werent given any filters, then we list all
if len(fs) == 0 {
as = append(as, &ResourceAddress{Index: -1})
}
// Filter each of the address. We keep track of this in a map to
// strip duplicates.
resultSet := make(map[string]*StateFilterResult)
for _, a := range as {
for _, r := range f.filterSingle(a) {
resultSet[r.String()] = r
}
}
// Make the result list
results := make([]*StateFilterResult, 0, len(resultSet))
for _, v := range resultSet {
results = append(results, v)
}
// Sort them and return
sort.Sort(StateFilterResultSlice(results))
return results, nil
}
func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
// The slice to keep track of results
var results []*StateFilterResult
// Go through modules first.
modules := make([]*ModuleState, 0, len(f.State.Modules))
for _, m := range f.State.Modules {
if f.relevant(a, m) {
modules = append(modules, m)
// Only add the module to the results if we haven't specified a type.
// We also ignore the root module.
if a.Type == "" && len(m.Path) > 1 {
results = append(results, &StateFilterResult{
Path: m.Path[1:],
Address: (&ResourceAddress{Path: m.Path[1:]}).String(),
Value: m,
})
}
}
}
// With the modules set, go through all the resources within
// the modules to find relevant resources.
for _, m := range modules {
for n, r := range m.Resources {
if f.relevant(a, r) {
// The name in the state contains valuable information. Parse.
key, err := ParseResourceStateKey(n)
if err != nil {
// If we get an error parsing, then just ignore it
// out of the state.
continue
}
if a.Index >= 0 && key.Index != a.Index {
// Index doesn't match
continue
}
if a.Name != "" && a.Name != key.Name {
continue
}
// Build the address for this resource
addr := &ResourceAddress{
Path: m.Path[1:],
Name: key.Name,
Type: key.Type,
Index: key.Index,
}
// Add the resource level result
resourceResult := &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Value: r,
}
if !a.InstanceTypeSet {
results = append(results, resourceResult)
}
// Add the instances
if r.Primary != nil {
addr.InstanceType = TypePrimary
addr.InstanceTypeSet = true
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: r.Primary,
})
}
for _, instance := range r.Tainted {
if f.relevant(a, instance) {
addr.InstanceType = TypeTainted
addr.InstanceTypeSet = true
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: instance,
})
}
}
for _, instance := range r.Deposed {
if f.relevant(a, instance) {
addr.InstanceType = TypeDeposed
addr.InstanceTypeSet = true
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: instance,
})
}
}
}
}
}
return results
}
// relevant checks for relevance of this address against the given value.
func (f *StateFilter) relevant(addr *ResourceAddress, raw interface{}) bool {
switch v := raw.(type) {
case *ModuleState:
path := v.Path[1:]
if len(addr.Path) > len(path) {
// Longer path in address means there is no way we match.
return false
}
// Check for a prefix match
for i, p := range addr.Path {
if path[i] != p {
// Any mismatches don't match.
return false
}
}
return true
case *ResourceState:
if addr.Type == "" {
// If we have no resource type, then we're interested in all!
return true
}
// If the type doesn't match we fail immediately
if v.Type != addr.Type {
return false
}
return true
default:
// If we don't know about it, let's just say no
return false
}
}
// StateFilterResult is a single result from a filter operation. Filter
// can match multiple things within a state (module, resource, instance, etc.)
// and this unifies that.
type StateFilterResult struct {
// Module path of the result
Path []string
// Address is the address that can be used to reference this exact result.
Address string
// Parent, if non-nil, is a parent of this result. For instances, the
// parent would be a resource. For resources, the parent would be
// a module. For modules, this is currently nil.
Parent *StateFilterResult
// Value is the actual value. This must be type switched on. It can be
// any data structures that `State` can hold: `ModuleState`,
// `ResourceState`, `InstanceState`.
Value interface{}
}
func (r *StateFilterResult) String() string {
return fmt.Sprintf("%T: %s", r.Value, r.Address)
}
func (r *StateFilterResult) sortedType() int {
switch r.Value.(type) {
case *ModuleState:
return 0
case *ResourceState:
return 1
case *InstanceState:
return 2
default:
return 50
}
}
// StateFilterResultSlice is a slice of results that implements
// sort.Interface. The sorting goal is what is most appealing to
// human output.
type StateFilterResultSlice []*StateFilterResult
func (s StateFilterResultSlice) Len() int { return len(s) }
func (s StateFilterResultSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s StateFilterResultSlice) Less(i, j int) bool {
a, b := s[i], s[j]
// If the addresses are different it is just lexographic sorting
if a.Address != b.Address {
return a.Address < b.Address
}
// Addresses are the same, which means it matters on the type
return a.sortedType() < b.sortedType()
}

View File

@ -0,0 +1,119 @@
package terraform
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestStateFilterFilter(t *testing.T) {
cases := map[string]struct {
State string
Filters []string
Expected []string
}{
"all": {
"small.tfstate",
[]string{},
[]string{
"*terraform.ResourceState: aws_key_pair.onprem",
"*terraform.InstanceState: aws_key_pair.onprem",
"*terraform.ModuleState: module.bootstrap",
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
"*terraform.ResourceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
"*terraform.InstanceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
},
},
"single resource": {
"small.tfstate",
[]string{"aws_key_pair.onprem"},
[]string{
"*terraform.ResourceState: aws_key_pair.onprem",
"*terraform.InstanceState: aws_key_pair.onprem",
},
},
"single instance": {
"small.tfstate",
[]string{"aws_key_pair.onprem.primary"},
[]string{
"*terraform.InstanceState: aws_key_pair.onprem",
},
},
"module filter": {
"complete.tfstate",
[]string{"module.bootstrap"},
[]string{
"*terraform.ModuleState: module.bootstrap",
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-a",
"*terraform.ResourceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
"*terraform.InstanceState: module.bootstrap.aws_route53_record.oasis-consul-bootstrap-ns",
"*terraform.ResourceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
"*terraform.InstanceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
},
},
"resource in module": {
"complete.tfstate",
[]string{"module.bootstrap.aws_route53_zone.oasis-consul-bootstrap"},
[]string{
"*terraform.ResourceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
"*terraform.InstanceState: module.bootstrap.aws_route53_zone.oasis-consul-bootstrap",
},
},
"resource in module 2": {
"resource-in-module-2.tfstate",
[]string{"module.foo.aws_instance.foo"},
[]string{},
},
"single count index": {
"complete.tfstate",
[]string{"module.consul.aws_instance.consul-green[0]"},
[]string{
"*terraform.ResourceState: module.consul.aws_instance.consul-green[0]",
"*terraform.InstanceState: module.consul.aws_instance.consul-green[0]",
},
},
}
for n, tc := range cases {
// Load our state
f, err := os.Open(filepath.Join("./test-fixtures", "state-filter", tc.State))
if err != nil {
t.Fatalf("%q: err: %s", n, err)
}
state, err := ReadState(f)
f.Close()
if err != nil {
t.Fatalf("%q: err: %s", n, err)
}
// Create the filter
filter := &StateFilter{State: state}
// Filter!
results, err := filter.Filter(tc.Filters...)
if err != nil {
t.Fatalf("%q: err: %s", n, err)
}
actual := make([]string, len(results))
for i, result := range results {
actual[i] = result.String()
}
if !reflect.DeepEqual(actual, tc.Expected) {
t.Fatalf("%q: expected, then actual\n\n%#v\n\n%#v", n, tc.Expected, actual)
}
}
}

View File

@ -76,6 +76,38 @@ func TestStateAddModule(t *testing.T) {
}
}
func TestStateOutputTypeRoundTrip(t *testing.T) {
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Outputs: map[string]interface{}{
"string_output": "String Value",
"list_output": []interface{}{"List", "Value"},
"map_output": map[string]interface{}{
"key1": "Map",
"key2": "Value",
},
},
},
},
}
buf := new(bytes.Buffer)
if err := WriteState(state, buf); err != nil {
t.Fatalf("err: %s", err)
}
roundTripped, err := ReadState(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(state, roundTripped) {
t.Fatalf("bad: %#v", roundTripped)
}
}
func TestStateModuleOrphans(t *testing.T) {
state := &State{
Modules: []*ModuleState{
@ -175,6 +207,35 @@ func TestStateModuleOrphans_deepNestedNilConfig(t *testing.T) {
}
}
func TestStateDeepCopy(t *testing.T) {
cases := []struct {
One, Two *State
F func(*State) interface{}
}{
// Version
{
&State{Version: 5},
&State{Version: 5},
func(s *State) interface{} { return s.Version },
},
// TFVersion
{
&State{TFVersion: "5"},
&State{TFVersion: "5"},
func(s *State) interface{} { return s.TFVersion },
},
}
for i, tc := range cases {
actual := tc.F(tc.One.DeepCopy())
expected := tc.F(tc.Two)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("Bad: %d\n\n%s\n\n%s", i, actual, expected)
}
}
}
func TestStateEqual(t *testing.T) {
cases := []struct {
Result bool
@ -348,6 +409,11 @@ func TestStateIncrementSerialMaybe(t *testing.T) {
},
5,
},
"S2 has a different TFVersion": {
&State{TFVersion: "0.1"},
&State{TFVersion: "0.2"},
1,
},
}
for name, tc := range cases {
@ -358,6 +424,277 @@ func TestStateIncrementSerialMaybe(t *testing.T) {
}
}
func TestStateRemove(t *testing.T) {
cases := map[string]struct {
Address string
One, Two *State
}{
"simple resource": {
"test_instance.foo",
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{},
},
},
},
},
"single instance": {
"test_instance.foo.primary",
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{},
},
},
},
},
"single instance in multi-count": {
"test_instance.foo[0]",
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo.0": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"test_instance.foo.1": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo.1": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
},
"single resource, multi-count": {
"test_instance.foo",
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo.0": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"test_instance.foo.1": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{},
},
},
},
},
"full module": {
"module.foo",
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
&ModuleState{
Path: []string{"root", "foo"},
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"test_instance.bar": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
},
"module and children": {
"module.foo",
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
&ModuleState{
Path: []string{"root", "foo"},
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"test_instance.bar": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
&ModuleState{
Path: []string{"root", "foo", "bar"},
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"test_instance.bar": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"test_instance.foo": &ResourceState{
Type: "test_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
},
}
for k, tc := range cases {
if err := tc.One.Remove(tc.Address); err != nil {
t.Fatalf("bad: %s\n\n%s", k, err)
}
if !tc.One.Equal(tc.Two) {
t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String())
}
}
}
func TestResourceStateEqual(t *testing.T) {
cases := []struct {
Result bool
@ -716,6 +1053,34 @@ func TestStateEmpty(t *testing.T) {
}
}
func TestStateFromFutureTerraform(t *testing.T) {
cases := []struct {
In string
Result bool
}{
{
"",
false,
},
{
"0.1",
false,
},
{
"999.15.1",
true,
},
}
for _, tc := range cases {
state := &State{TFVersion: tc.In}
actual := state.FromFutureTerraform()
if actual != tc.Result {
t.Fatalf("%s: bad: %v", tc.In, actual)
}
}
}
func TestStateIsRemote(t *testing.T) {
cases := []struct {
In *State
@ -829,16 +1194,43 @@ func TestInstanceState_MergeDiff_nilDiff(t *testing.T) {
}
}
func TestReadUpgradeStateV1toV2(t *testing.T) {
// ReadState should transparently detect the old version but will upgrade
// it on Write.
actual, err := ReadState(strings.NewReader(testV1State))
if err != nil {
t.Fatalf("err: %s", err)
}
buf := new(bytes.Buffer)
if err := WriteState(actual, buf); err != nil {
t.Fatalf("err: %s", err)
}
if actual.Version != 2 {
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
}
roundTripped, err := ReadState(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, roundTripped) {
t.Fatalf("bad: %#v", actual)
}
}
func TestReadUpgradeState(t *testing.T) {
state := &StateV1{
Resources: map[string]*ResourceStateV1{
"foo": &ResourceStateV1{
state := &StateV0{
Resources: map[string]*ResourceStateV0{
"foo": &ResourceStateV0{
ID: "bar",
},
},
}
buf := new(bytes.Buffer)
if err := testWriteStateV1(state, buf); err != nil {
if err := testWriteStateV0(state, buf); err != nil {
t.Fatalf("err: %s", err)
}
@ -849,7 +1241,7 @@ func TestReadUpgradeState(t *testing.T) {
t.Fatalf("err: %s", err)
}
upgraded, err := upgradeV1State(state)
upgraded, err := upgradeV0State(state)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -935,20 +1327,111 @@ func TestReadStateNewVersion(t *testing.T) {
}
}
func TestUpgradeV1State(t *testing.T) {
old := &StateV1{
func TestReadStateTFVersion(t *testing.T) {
type tfVersion struct {
TFVersion string `json:"terraform_version"`
}
cases := []struct {
Written string
Read string
Err bool
}{
{
"0.0.0",
"0.0.0",
false,
},
{
"",
"",
false,
},
{
"bad",
"",
true,
},
}
for _, tc := range cases {
buf, err := json.Marshal(&tfVersion{tc.Written})
if err != nil {
t.Fatalf("err: %v", err)
}
s, err := ReadState(bytes.NewReader(buf))
if (err != nil) != tc.Err {
t.Fatalf("%s: err: %s", tc.Written, err)
}
if err != nil {
continue
}
if s.TFVersion != tc.Read {
t.Fatalf("%s: bad: %s", tc.Written, s.TFVersion)
}
}
}
func TestWriteStateTFVersion(t *testing.T) {
cases := []struct {
Write string
Read string
Err bool
}{
{
"0.0.0",
"0.0.0",
false,
},
{
"",
"",
false,
},
{
"bad",
"",
true,
},
}
for _, tc := range cases {
var buf bytes.Buffer
err := WriteState(&State{TFVersion: tc.Write}, &buf)
if (err != nil) != tc.Err {
t.Fatalf("%s: err: %s", tc.Write, err)
}
if err != nil {
continue
}
s, err := ReadState(&buf)
if err != nil {
t.Fatalf("%s: err: %s", tc.Write, err)
}
if s.TFVersion != tc.Read {
t.Fatalf("%s: bad: %s", tc.Write, s.TFVersion)
}
}
}
func TestUpgradeV0State(t *testing.T) {
old := &StateV0{
Outputs: map[string]string{
"ip": "127.0.0.1",
},
Resources: map[string]*ResourceStateV1{
"foo": &ResourceStateV1{
Resources: map[string]*ResourceStateV0{
"foo": &ResourceStateV0{
Type: "test_resource",
ID: "bar",
Attributes: map[string]string{
"key": "val",
},
},
"bar": &ResourceStateV1{
"bar": &ResourceStateV0{
Type: "test_resource",
ID: "1234",
Attributes: map[string]string{
@ -960,7 +1443,7 @@ func TestUpgradeV1State(t *testing.T) {
"bar": struct{}{},
},
}
state, err := upgradeV1State(old)
state, err := upgradeV0State(old)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -1062,3 +1545,34 @@ func TestParseResourceStateKey(t *testing.T) {
}
}
}
const testV1State = `{
"version": 1,
"serial": 9,
"remote": {
"type": "http",
"config": {
"url": "http://my-cool-server.com/"
}
},
"modules": [
{
"path": [
"root"
],
"outputs": null,
"resources": {
"foo": {
"type": "",
"primary": {
"id": "bar"
}
}
},
"depends_on": [
"aws_instance.bar"
]
}
]
}
`

View File

@ -21,21 +21,21 @@ const (
stateFormatVersion byte = 1
)
// StateV1 is used to represent the state of Terraform files before
// StateV0 is used to represent the state of Terraform files before
// 0.3. It is automatically upgraded to a modern State representation
// on start.
type StateV1 struct {
type StateV0 struct {
Outputs map[string]string
Resources map[string]*ResourceStateV1
Resources map[string]*ResourceStateV0
Tainted map[string]struct{}
once sync.Once
}
func (s *StateV1) init() {
func (s *StateV0) init() {
s.once.Do(func() {
if s.Resources == nil {
s.Resources = make(map[string]*ResourceStateV1)
s.Resources = make(map[string]*ResourceStateV0)
}
if s.Tainted == nil {
@ -44,8 +44,8 @@ func (s *StateV1) init() {
})
}
func (s *StateV1) deepcopy() *StateV1 {
result := new(StateV1)
func (s *StateV0) deepcopy() *StateV0 {
result := new(StateV0)
result.init()
if s != nil {
for k, v := range s.Resources {
@ -61,7 +61,7 @@ func (s *StateV1) deepcopy() *StateV1 {
// prune is a helper that removes any empty IDs from the state
// and cleans it up in general.
func (s *StateV1) prune() {
func (s *StateV0) prune() {
for k, v := range s.Resources {
if v.ID == "" {
delete(s.Resources, k)
@ -72,7 +72,7 @@ func (s *StateV1) prune() {
// Orphans returns a list of keys of resources that are in the State
// but aren't present in the configuration itself. Hence, these keys
// represent the state of resources that are orphans.
func (s *StateV1) Orphans(c *config.Config) []string {
func (s *StateV0) Orphans(c *config.Config) []string {
keys := make(map[string]struct{})
for k, _ := range s.Resources {
keys[k] = struct{}{}
@ -96,7 +96,7 @@ func (s *StateV1) Orphans(c *config.Config) []string {
return result
}
func (s *StateV1) String() string {
func (s *StateV0) String() string {
if len(s.Resources) == 0 {
return "<no state>"
}
@ -175,7 +175,7 @@ func (s *StateV1) String() string {
//
// Extra is just extra data that a provider can return that we store
// for later, but is not exposed in any way to the user.
type ResourceStateV1 struct {
type ResourceStateV0 struct {
// This is filled in and managed by Terraform, and is the resource
// type itself such as "mycloud_instance". If a resource provider sets
// this value, it won't be persisted.
@ -228,8 +228,8 @@ type ResourceStateV1 struct {
// If the diff attribute requires computing the value, and hence
// won't be available until apply, the value is replaced with the
// computeID.
func (s *ResourceStateV1) MergeDiff(d *InstanceDiff) *ResourceStateV1 {
var result ResourceStateV1
func (s *ResourceStateV0) MergeDiff(d *InstanceDiff) *ResourceStateV0 {
var result ResourceStateV0
if s != nil {
result = *s
}
@ -258,7 +258,7 @@ func (s *ResourceStateV1) MergeDiff(d *InstanceDiff) *ResourceStateV1 {
return &result
}
func (s *ResourceStateV1) GoString() string {
func (s *ResourceStateV0) GoString() string {
return fmt.Sprintf("*%#v", *s)
}
@ -270,10 +270,10 @@ type ResourceDependency struct {
ID string
}
// ReadStateV1 reads a state structure out of a reader in the format that
// ReadStateV0 reads a state structure out of a reader in the format that
// was written by WriteState.
func ReadStateV1(src io.Reader) (*StateV1, error) {
var result *StateV1
func ReadStateV0(src io.Reader) (*StateV0, error) {
var result *StateV0
var err error
n := 0

View File

@ -12,10 +12,10 @@ import (
"github.com/mitchellh/hashstructure"
)
func TestReadWriteStateV1(t *testing.T) {
state := &StateV1{
Resources: map[string]*ResourceStateV1{
"foo": &ResourceStateV1{
func TestReadWriteStateV0(t *testing.T) {
state := &StateV0{
Resources: map[string]*ResourceStateV0{
"foo": &ResourceStateV0{
ID: "bar",
ConnInfo: map[string]string{
"type": "ssh",
@ -33,7 +33,7 @@ func TestReadWriteStateV1(t *testing.T) {
}
buf := new(bytes.Buffer)
if err := testWriteStateV1(state, buf); err != nil {
if err := testWriteStateV0(state, buf); err != nil {
t.Fatalf("err: %s", err)
}
@ -47,7 +47,7 @@ func TestReadWriteStateV1(t *testing.T) {
t.Fatalf("structure changed during serialization!")
}
actual, err := ReadStateV1(buf)
actual, err := ReadStateV0(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -75,9 +75,9 @@ func (s *sensitiveState) init() {
})
}
// testWriteStateV1 writes a state somewhere in a binary format.
// testWriteStateV0 writes a state somewhere in a binary format.
// Only for testing now
func testWriteStateV1(d *StateV1, dst io.Writer) error {
func testWriteStateV0(d *StateV0, dst io.Writer) error {
// Write the magic bytes so we can determine the file format later
n, err := dst.Write([]byte(stateFormatMagic))
if err != nil {

View File

@ -592,7 +592,7 @@ aws_instance.foo:
Outputs:
foo_num = bar,bar,bar
foo_num = [bar,bar,bar]
`
const testTerraformApplyOutputMultiStr = `

Some files were not shown because too many files have changed in this diff Show More