Merge pull request #6596 from hashicorp/merge-dev-0.7
Merge rebased dev-0.7 branch into master
This commit is contained in:
commit
4faa6b37e4
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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:
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
5
Makefile
5
Makefile
|
@ -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 .
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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{}
|
|
@ -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) },
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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.`
|
|
@ -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
|
||||
`
|
|
@ -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.`
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
`
|
33
commands.go
33
commands.go
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
96
config.go
96
config.go
|
@ -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 == "" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
module "foo" {
|
||||
source = "./foo"
|
||||
nodes = [1,2,3]
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
module "foo" {
|
||||
source = "./foo"
|
||||
nodes = [1,2,3]
|
||||
nodes = {
|
||||
key1 = "value1"
|
||||
key2 = "value2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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{}) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
main.go
8
main.go
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
339
plugin/client.go
339
plugin/client.go
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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{},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
}
|
138
plugin/server.go
138
plugin/server.go
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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",
|
|
@ -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.
|
|
@ -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")
|
108
rpc/client.go
108
rpc/client.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
35
rpc/rpc.go
35
rpc/rpc.go
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
147
rpc/server.go
147
rpc/server.go
|
@ -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)
|
||||
}
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
`
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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 {
|
|
@ -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
Loading…
Reference in New Issue