From 3ea3c657b5d0a5974517756c2ee41fb4998647b2 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Wed, 11 May 2016 20:05:02 -0400 Subject: [PATCH] core: Use OutputState in JSON instead of map This commit forward ports the changes made for 0.6.17, in order to store the type and sensitive flag against outputs. It also refactors the logic of the import for V0 to V1 state, and fixes up the call sites of the new format for outputs in V2 state. Finally we fix up tests which did not previously set a state version where one is required. --- .../template/resource_template_file_test.go | 4 +- .../providers/terraform/data_source_state.go | 6 +- .../tls/resource_cert_request_test.go | 2 +- .../tls/resource_locally_signed_cert_test.go | 2 +- .../tls/resource_private_key_test.go | 20 +- .../tls/resource_self_signed_cert_test.go | 2 +- command/apply.go | 2 +- command/command_test.go | 1 + command/output.go | 4 +- command/output_test.go | 56 ++- helper/resource/testing.go | 2 +- state/remote/atlas.go | 1 + state/remote/atlas_test.go | 25 +- state/testing.go | 26 +- terraform/context_apply_test.go | 26 +- terraform/context_refresh_test.go | 27 +- terraform/eval_output.go | 23 +- terraform/graph_config_node_output.go | 5 +- terraform/interpolate.go | 4 +- terraform/interpolate_test.go | 7 +- terraform/state.go | 189 ++++++---- terraform/state_add_test.go | 16 +- terraform/state_test.go | 86 ++++- terraform/state_v0.go | 55 +++ terraform/state_v1.go | 334 ++++++++++++++++++ terraform/transform_output_test.go | 12 +- 26 files changed, 786 insertions(+), 151 deletions(-) create mode 100644 terraform/state_v1.go diff --git a/builtin/providers/template/resource_template_file_test.go b/builtin/providers/template/resource_template_file_test.go index ed0d3a68c..05e88c4db 100644 --- a/builtin/providers/template/resource_template_file_test.go +++ b/builtin/providers/template/resource_template_file_test.go @@ -36,7 +36,7 @@ func TestTemplateRendering(t *testing.T) { Config: testTemplateConfig(tt.template, tt.vars), Check: func(s *terraform.State) error { got := s.RootModule().Outputs["rendered"] - if tt.want != got { + if tt.want != got.Value { return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", tt.template, tt.vars, got, tt.want) } return nil @@ -65,7 +65,7 @@ func TestTemplateVariableChange(t *testing.T) { Check: func(i int, want string) r.TestCheckFunc { return func(s *terraform.State) error { got := s.RootModule().Outputs["rendered"] - if want != got { + if want != got.Value { return fmt.Errorf("[%d] got:\n%q\nwant:\n%q\n", i, got, want) } return nil diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index 68369eaae..513a3c235 100644 --- a/builtin/providers/terraform/data_source_state.go +++ b/builtin/providers/terraform/data_source_state.go @@ -54,7 +54,11 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error { var outputs map[string]interface{} if !state.State().Empty() { - outputs = state.State().RootModule().Outputs + outputValueMap := make(map[string]string) + for key, output := range state.State().RootModule().Outputs { + //This is ok for 0.6.17 as outputs will have been strings + outputValueMap[key] = output.Value.(string) + } } d.SetId(time.Now().UTC().String()) diff --git a/builtin/providers/tls/resource_cert_request_test.go b/builtin/providers/tls/resource_cert_request_test.go index 2c2c4f5d4..c31b8d6a1 100644 --- a/builtin/providers/tls/resource_cert_request_test.go +++ b/builtin/providers/tls/resource_cert_request_test.go @@ -50,7 +50,7 @@ EOT } `, testPrivateKey), Check: func(s *terraform.State) error { - gotUntyped := s.RootModule().Outputs["key_pem"] + gotUntyped := s.RootModule().Outputs["key_pem"].Value got, ok := gotUntyped.(string) if !ok { diff --git a/builtin/providers/tls/resource_locally_signed_cert_test.go b/builtin/providers/tls/resource_locally_signed_cert_test.go index aa705ece8..3e051b878 100644 --- a/builtin/providers/tls/resource_locally_signed_cert_test.go +++ b/builtin/providers/tls/resource_locally_signed_cert_test.go @@ -47,7 +47,7 @@ EOT } `, testCertRequest, testCACert, testCAPrivateKey), Check: func(s *terraform.State) error { - gotUntyped := s.RootModule().Outputs["cert_pem"] + gotUntyped := s.RootModule().Outputs["cert_pem"].Value got, ok := gotUntyped.(string) if !ok { return fmt.Errorf("output for \"cert_pem\" is not a string") diff --git a/builtin/providers/tls/resource_private_key_test.go b/builtin/providers/tls/resource_private_key_test.go index cec3a8198..5c25be87d 100644 --- a/builtin/providers/tls/resource_private_key_test.go +++ b/builtin/providers/tls/resource_private_key_test.go @@ -29,7 +29,7 @@ func TestPrivateKeyRSA(t *testing.T) { } `, Check: func(s *terraform.State) error { - gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"] + gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value gotPrivate, ok := gotPrivateUntyped.(string) if !ok { return fmt.Errorf("output for \"private_key_pem\" is not a string") @@ -42,7 +42,7 @@ func TestPrivateKeyRSA(t *testing.T) { return fmt.Errorf("private key PEM looks too long for a 2048-bit key (got %v characters)", len(gotPrivate)) } - gotPublicUntyped := s.RootModule().Outputs["public_key_pem"] + gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value gotPublic, ok := gotPublicUntyped.(string) if !ok { return fmt.Errorf("output for \"public_key_pem\" is not a string") @@ -51,7 +51,7 @@ func TestPrivateKeyRSA(t *testing.T) { return fmt.Errorf("public key is missing public key PEM preamble") } - gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"] + gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"].Value gotPublicSSH, ok := gotPublicSSHUntyped.(string) if !ok { return fmt.Errorf("output for \"public_key_openssh\" is not a string") @@ -74,7 +74,7 @@ func TestPrivateKeyRSA(t *testing.T) { } `, Check: func(s *terraform.State) error { - gotUntyped := s.RootModule().Outputs["key_pem"] + gotUntyped := s.RootModule().Outputs["key_pem"].Value got, ok := gotUntyped.(string) if !ok { return fmt.Errorf("output for \"key_pem\" is not a string") @@ -112,7 +112,7 @@ func TestPrivateKeyECDSA(t *testing.T) { } `, Check: func(s *terraform.State) error { - gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"] + gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value gotPrivate, ok := gotPrivateUntyped.(string) if !ok { return fmt.Errorf("output for \"private_key_pem\" is not a string") @@ -122,7 +122,7 @@ func TestPrivateKeyECDSA(t *testing.T) { return fmt.Errorf("Private key is missing EC key PEM preamble") } - gotPublicUntyped := s.RootModule().Outputs["public_key_pem"] + gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value gotPublic, ok := gotPublicUntyped.(string) if !ok { return fmt.Errorf("output for \"public_key_pem\" is not a string") @@ -132,7 +132,7 @@ func TestPrivateKeyECDSA(t *testing.T) { return fmt.Errorf("public key is missing public key PEM preamble") } - gotPublicSSH := s.RootModule().Outputs["public_key_openssh"] + gotPublicSSH := s.RootModule().Outputs["public_key_openssh"].Value.(string) if gotPublicSSH != "" { return fmt.Errorf("P224 EC key should not generate OpenSSH public key") } @@ -157,7 +157,7 @@ func TestPrivateKeyECDSA(t *testing.T) { } `, Check: func(s *terraform.State) error { - gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"] + gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value gotPrivate, ok := gotPrivateUntyped.(string) if !ok { return fmt.Errorf("output for \"private_key_pem\" is not a string") @@ -166,7 +166,7 @@ func TestPrivateKeyECDSA(t *testing.T) { return fmt.Errorf("Private key is missing EC key PEM preamble") } - gotPublicUntyped := s.RootModule().Outputs["public_key_pem"] + gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value gotPublic, ok := gotPublicUntyped.(string) if !ok { return fmt.Errorf("output for \"public_key_pem\" is not a string") @@ -175,7 +175,7 @@ func TestPrivateKeyECDSA(t *testing.T) { return fmt.Errorf("public key is missing public key PEM preamble") } - gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"] + gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"].Value gotPublicSSH, ok := gotPublicSSHUntyped.(string) if !ok { return fmt.Errorf("output for \"public_key_openssh\" is not a string") diff --git a/builtin/providers/tls/resource_self_signed_cert_test.go b/builtin/providers/tls/resource_self_signed_cert_test.go index b403956f4..22cc66032 100644 --- a/builtin/providers/tls/resource_self_signed_cert_test.go +++ b/builtin/providers/tls/resource_self_signed_cert_test.go @@ -60,7 +60,7 @@ EOT } `, testPrivateKey), Check: func(s *terraform.State) error { - gotUntyped := s.RootModule().Outputs["key_pem"] + gotUntyped := s.RootModule().Outputs["key_pem"].Value got, ok := gotUntyped.(string) if !ok { return fmt.Errorf("output for \"public_key_openssh\" is not a string") diff --git a/command/apply.go b/command/apply.go index 5598d5c35..83ae29a7f 100644 --- a/command/apply.go +++ b/command/apply.go @@ -415,7 +415,7 @@ func outputsAsString(state *terraform.State, schema []*config.Output, includeHea } v := outputs[k] - switch typedV := v.(type) { + switch typedV := v.Value.(type) { case string: outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV)) case []interface{}: diff --git a/command/command_test.go b/command/command_test.go index fcc8dfb25..283395df0 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -116,6 +116,7 @@ func testReadPlan(t *testing.T, path string) *terraform.Plan { // testState returns a test State structure that we use for a lot of tests. func testState() *terraform.State { return &terraform.State{ + Version: 2, Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, diff --git a/command/output.go b/command/output.go index 5808bae77..c40b94e3c 100644 --- a/command/output.go +++ b/command/output.go @@ -95,7 +95,7 @@ func (c *OutputCommand) Run(args []string) int { return 1 } - switch output := v.(type) { + switch output := v.Value.(type) { case string: c.Ui.Output(output) return 0 @@ -137,7 +137,7 @@ func (c *OutputCommand) Run(args []string) int { return 1 } default: - panic(fmt.Errorf("Unknown output type: %T", output)) + panic(fmt.Errorf("Unknown output type: %T", v.Value.(string))) } return 0 diff --git a/command/output_test.go b/command/output_test.go index 9c79f82ca..c553ff5aa 100644 --- a/command/output_test.go +++ b/command/output_test.go @@ -16,8 +16,11 @@ func TestOutput(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Value: "bar", + Type: "string", + }, }, }, }, @@ -52,14 +55,20 @@ func TestModuleOutput(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Value: "bar", + Type: "string", + }, }, }, &terraform.ModuleState{ Path: []string{"root", "my_module"}, - Outputs: map[string]interface{}{ - "blah": "tastatur", + Outputs: map[string]*terraform.OutputState{ + "blah": &terraform.OutputState{ + Value: "tastatur", + Type: "string", + }, }, }, }, @@ -96,8 +105,11 @@ func TestMissingModuleOutput(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Value: "bar", + Type: "string", + }, }, }, }, @@ -129,8 +141,11 @@ func TestOutput_badVar(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Value: "bar", + Type: "string", + }, }, }, }, @@ -160,9 +175,15 @@ func TestOutput_blank(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{ - "foo": "bar", - "name": "john-doe", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Value: "bar", + Type: "string", + }, + "name": &terraform.OutputState{ + Value: "john-doe", + Type: "string", + }, }, }, }, @@ -253,7 +274,7 @@ func TestOutput_noVars(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{}, + Outputs: map[string]*terraform.OutputState{}, }, }, } @@ -282,8 +303,11 @@ func TestOutput_stateDefault(t *testing.T) { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Value: "bar", + Type: "string", + }, }, }, }, diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 17358f93d..a6ceec140 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -565,7 +565,7 @@ func TestCheckOutput(name, value string) TestCheckFunc { return fmt.Errorf("Not found: %s", name) } - if rs != value { + if rs.Value != value { return fmt.Errorf( "Output '%s': expected %#v, got %#v", name, diff --git a/state/remote/atlas.go b/state/remote/atlas.go index 6a48c21bc..24e81f177 100644 --- a/state/remote/atlas.go +++ b/state/remote/atlas.go @@ -322,6 +322,7 @@ func (c *AtlasClient) handleConflict(msg string, state []byte) error { var buf bytes.Buffer if err := terraform.WriteState(proposedState, &buf); err != nil { return conflictHandlingError(err) + } return c.Put(buf.Bytes()) } else { diff --git a/state/remote/atlas_test.go b/state/remote/atlas_test.go index 47b019bd2..701fbcbd8 100644 --- a/state/remote/atlas_test.go +++ b/state/remote/atlas_test.go @@ -87,6 +87,7 @@ func TestAtlasClient_NoConflict(t *testing.T) { if err := terraform.WriteState(state, &stateJson); err != nil { t.Fatalf("err: %s", err) } + if err := client.Put(stateJson.Bytes()); err != nil { t.Fatalf("err: %s", err) } @@ -111,7 +112,11 @@ func TestAtlasClient_LegitimateConflict(t *testing.T) { } // Changing the state but not the serial. Should generate a conflict. - state.RootModule().Outputs["drift"] = "happens" + state.RootModule().Outputs["drift"] = &terraform.OutputState{ + Type: "string", + Sensitive: false, + Value: "happens", + } var stateJson bytes.Buffer if err := terraform.WriteState(state, &stateJson); err != nil { @@ -255,7 +260,11 @@ var testStateModuleOrderChange = []byte( "grandchild" ], "outputs": { - "foo": "bar2" + "foo": { + "sensitive": false, + "type": "string", + "value": "bar" + } }, "resources": null }, @@ -266,7 +275,11 @@ var testStateModuleOrderChange = []byte( "grandchild" ], "outputs": { - "foo": "bar1" + "foo": { + "sensitive": false, + "type": "string", + "value": "bar" + } }, "resources": null } @@ -284,7 +297,11 @@ var testStateSimple = []byte( "root" ], "outputs": { - "foo": "bar" + "foo": { + "sensitive": false, + "type": "string", + "value": "bar" + } }, "resources": null } diff --git a/state/testing.go b/state/testing.go index c5305ecef..357ea234a 100644 --- a/state/testing.go +++ b/state/testing.go @@ -36,8 +36,12 @@ 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]interface{}{ - "bar": "baz", + Outputs: map[string]*terraform.OutputState{ + "bar": &terraform.OutputState{ + Type: "string", + Sensitive: false, + Value: "baz", + }, }, }) @@ -93,8 +97,14 @@ func TestState(t *testing.T, s interface{}) { current = ¤tCopy current.Modules = []*terraform.ModuleState{ &terraform.ModuleState{ - Path: []string{"root", "somewhere"}, - Outputs: map[string]interface{}{"serialCheck": "true"}, + Path: []string{"root", "somewhere"}, + Outputs: map[string]*terraform.OutputState{ + "serialCheck": &terraform.OutputState{ + Type: "string", + Sensitive: false, + Value: "true", + }, + }, }, } if err := writer.WriteState(current); err != nil { @@ -123,8 +133,12 @@ func TestStateInitial() *terraform.State { Modules: []*terraform.ModuleState{ &terraform.ModuleState{ Path: []string{"root", "child"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*terraform.OutputState{ + "foo": &terraform.OutputState{ + Type: "string", + Sensitive: false, + Value: "bar", + }, }, }, }, diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index ca172ca1f..71b48d100 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -1008,8 +1008,12 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) { }, }, }, - Outputs: map[string]interface{}{ - "a_output": "a", + Outputs: map[string]*OutputState{ + "a_output": &OutputState{ + Type: "string", + Sensitive: false, + Value: "a", + }, }, }, }, @@ -1408,7 +1412,7 @@ func TestContext2Apply_multiVar(t *testing.T) { actual := state.RootModule().Outputs["output"] expected := "bar0,bar1,bar2" - if actual != expected { + if actual.Value != expected { t.Fatalf("bad: \n%s", actual) } @@ -1436,7 +1440,7 @@ func TestContext2Apply_multiVar(t *testing.T) { actual := state.RootModule().Outputs["output"] expected := "bar0" - if actual != expected { + if actual.Value != expected { t.Fatalf("bad: \n%s", actual) } } @@ -1477,9 +1481,17 @@ func TestContext2Apply_outputOrphan(t *testing.T) { Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, - Outputs: map[string]interface{}{ - "foo": "bar", - "bar": "baz", + Outputs: map[string]*OutputState{ + "foo": &OutputState{ + Type: "string", + Sensitive: false, + Value: "bar", + }, + "bar": &OutputState{ + Type: "string", + Sensitive: false, + Value: "baz", + }, }, }, }, diff --git a/terraform/context_refresh_test.go b/terraform/context_refresh_test.go index 3d46c27c2..4e9bd4367 100644 --- a/terraform/context_refresh_test.go +++ b/terraform/context_refresh_test.go @@ -452,8 +452,12 @@ func TestContext2Refresh_output(t *testing.T) { }, }, - Outputs: map[string]interface{}{ - "foo": "foo", + Outputs: map[string]*OutputState{ + "foo": &OutputState{ + Type: "string", + Sensitive: false, + Value: "foo", + }, }, }, }, @@ -738,9 +742,15 @@ func TestContext2Refresh_orphanModule(t *testing.T) { }, }, }, - Outputs: map[string]interface{}{ - "id": "i-bcd234", - "grandchild_id": "i-cde345", + Outputs: map[string]*OutputState{ + "id": &OutputState{ + Value: "i-bcd234", + Type: "string", + }, + "grandchild_id": &OutputState{ + Value: "i-cde345", + Type: "string", + }, }, }, &ModuleState{ @@ -752,8 +762,11 @@ func TestContext2Refresh_orphanModule(t *testing.T) { }, }, }, - Outputs: map[string]interface{}{ - "id": "i-cde345", + Outputs: map[string]*OutputState{ + "id": &OutputState{ + Value: "i-cde345", + Type: "string", + }, }, }, }, diff --git a/terraform/eval_output.go b/terraform/eval_output.go index b584bdecc..bee4f1084 100644 --- a/terraform/eval_output.go +++ b/terraform/eval_output.go @@ -38,8 +38,9 @@ func (n *EvalDeleteOutput) Eval(ctx EvalContext) (interface{}, error) { // EvalWriteOutput is an EvalNode implementation that writes the output // for the given name to the current state. type EvalWriteOutput struct { - Name string - Value *config.RawConfig + Name string + Sensitive bool + Value *config.RawConfig } // TODO: test @@ -80,11 +81,23 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { switch valueTyped := valueRaw.(type) { case string: - mod.Outputs[n.Name] = valueTyped + mod.Outputs[n.Name] = &OutputState{ + Type: "string", + Sensitive: n.Sensitive, + Value: valueTyped, + } case []interface{}: - mod.Outputs[n.Name] = valueTyped + mod.Outputs[n.Name] = &OutputState{ + Type: "list", + Sensitive: n.Sensitive, + Value: valueTyped, + } case map[string]interface{}: - mod.Outputs[n.Name] = valueTyped + mod.Outputs[n.Name] = &OutputState{ + Type: "map", + Sensitive: n.Sensitive, + Value: valueTyped, + } default: return nil, fmt.Errorf("output %s is not a valid type (%T)\n", n.Name, valueTyped) } diff --git a/terraform/graph_config_node_output.go b/terraform/graph_config_node_output.go index d4f00451c..8410102d2 100644 --- a/terraform/graph_config_node_output.go +++ b/terraform/graph_config_node_output.go @@ -48,8 +48,9 @@ func (n *GraphNodeConfigOutput) EvalTree() EvalNode { Node: &EvalSequence{ Nodes: []EvalNode{ &EvalWriteOutput{ - Name: n.Output.Name, - Value: n.Output.RawConfig, + Name: n.Output.Name, + Sensitive: n.Output.Sensitive, + Value: n.Output.RawConfig, }, }, }, diff --git a/terraform/interpolate.go b/terraform/interpolate.go index cb19d4eb6..0a627aca6 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -161,8 +161,8 @@ func (i *Interpolater) valueModuleVar( result[n] = unknownVariable() } else { // Get the value from the outputs - if value, ok := mod.Outputs[v.Field]; ok { - output, err := hil.InterfaceToVariable(value) + if outputState, ok := mod.Outputs[v.Field]; ok { + output, err := hil.InterfaceToVariable(outputState.Value) if err != nil { return err } diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go index 9485512a2..d85f7b36c 100644 --- a/terraform/interpolate_test.go +++ b/terraform/interpolate_test.go @@ -68,8 +68,11 @@ func TestInterpolater_moduleVariable(t *testing.T) { }, &ModuleState{ Path: []string{RootModuleName, "child"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*OutputState{ + "foo": &OutputState{ + Type: "string", + Value: "bar", + }, }, }, }, diff --git a/terraform/state.go b/terraform/state.go index 6edff0e46..12fb901e4 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -6,7 +6,7 @@ import ( "encoding/json" "fmt" "io" - "log" + "io/ioutil" "reflect" "sort" "strconv" @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/config" + "github.com/mitchellh/copystructure" ) const ( @@ -547,6 +548,69 @@ func (r *RemoteState) GoString() string { return fmt.Sprintf("*%#v", *r) } +// OutputState is used to track the state relevant to a single output. +type OutputState struct { + // Sensitive describes whether the output is considered sensitive, + // which may lead to masking the value on screen in some cases. + Sensitive bool `json:"sensitive"` + // Type describes the structure of Value. Valid values are "string", + // "map" and "list" + Type string `json:"type"` + // Value contains the value of the output, in the structure described + // by the Type field. + Value interface{} `json:"value"` +} + +func (s *OutputState) String() string { + // This is a v0.6.x implementation only + return fmt.Sprintf("%s", s.Value.(string)) +} + +// Equal compares two OutputState structures for equality. nil values are +// considered equal. +func (s *OutputState) Equal(other *OutputState) bool { + if s == nil && other == nil { + return true + } + + if s == nil || other == nil { + return false + } + + if s.Type != other.Type { + return false + } + + if s.Sensitive != other.Sensitive { + return false + } + + if !reflect.DeepEqual(s.Value, other.Value) { + return false + } + + return true +} + +func (s *OutputState) deepcopy() *OutputState { + if s == nil { + return nil + } + + valueCopy, err := copystructure.Copy(s.Value) + if err != nil { + panic(fmt.Errorf("Error copying output value: %s", err)) + } + + n := &OutputState{ + Type: s.Type, + Sensitive: s.Sensitive, + Value: valueCopy, + } + + return n +} + // ModuleState is used to track all the state relevant to a single // module. Previous to Terraform 0.3, all state belonged to the "root" // module. @@ -558,7 +622,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]interface{} `json:"outputs"` + Outputs map[string]*OutputState `json:"outputs"` // Resources is a mapping of the logically named resource to // the state of the resource. Each resource may actually have @@ -593,7 +657,7 @@ func (m *ModuleState) Equal(other *ModuleState) bool { return false } for k, v := range m.Outputs { - if !reflect.DeepEqual(other.Outputs[k], v) { + if !other.Outputs[k].Equal(v) { return false } } @@ -683,7 +747,7 @@ func (m *ModuleState) View(id string) *ModuleState { func (m *ModuleState) init() { if m.Outputs == nil { - m.Outputs = make(map[string]interface{}) + m.Outputs = make(map[string]*OutputState) } if m.Resources == nil { m.Resources = make(map[string]*ResourceState) @@ -696,14 +760,14 @@ func (m *ModuleState) deepcopy() *ModuleState { } n := &ModuleState{ Path: make([]string, len(m.Path)), - Outputs: make(map[string]interface{}, len(m.Outputs)), + Outputs: make(map[string]*OutputState, len(m.Outputs)), Resources: make(map[string]*ResourceState, len(m.Resources)), Dependencies: make([]string, len(m.Dependencies)), } copy(n.Path, m.Path) copy(n.Dependencies, m.Dependencies) for k, v := range m.Outputs { - n.Outputs[k] = v + n.Outputs[k] = v.deepcopy() } for k, v := range m.Resources { n.Resources[k] = v.deepcopy() @@ -722,7 +786,7 @@ func (m *ModuleState) prune() { } for k, v := range m.Outputs { - if v == config.UnknownVariableValue { + if v.Value == config.UnknownVariableValue { delete(m.Outputs, k) } } @@ -823,7 +887,7 @@ func (m *ModuleState) String() string { for _, k := range ks { v := m.Outputs[k] - switch vTyped := v.(type) { + switch vTyped := v.Value.(type) { case string: buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) case []interface{}: @@ -1386,6 +1450,10 @@ func (e *EphemeralState) DeepCopy() *EphemeralState { return n } +type jsonStateVersionIdentifier struct { + Version int `json:"version"` +} + // ReadState reads a state structure out of a reader in the format that // was written by WriteState. func ReadState(src io.Reader) (*State, error) { @@ -1402,14 +1470,59 @@ func ReadState(src io.Reader) (*State, error) { if err != nil { return nil, err } - return upgradeV0State(old) + return old.upgrade() } - // 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) + // If we are JSON we buffer the whole thing in memory so we can read it twice. + // This is suboptimal, but will work for now. + jsonBytes, err := ioutil.ReadAll(buf) + if err != nil { + return nil, fmt.Errorf("Reading state file failed: %v", err) + } + + versionIdentifier := &jsonStateVersionIdentifier{} + if err := json.Unmarshal(jsonBytes, versionIdentifier); err != nil { + return nil, fmt.Errorf("Decoding state file version failed: %v", err) + } + + switch versionIdentifier.Version { + case 0: + return nil, fmt.Errorf("State version 0 is not supported as JSON.") + case 1: + old, err := ReadStateV1(jsonBytes) + if err != nil { + return nil, err + } + return old.upgrade() + case 2: + state, err := ReadStateV2(jsonBytes) + if err != nil { + return nil, err + } + return state, nil + default: + return nil, fmt.Errorf("State version %d not supported, please update.", + versionIdentifier.Version) + } +} + +func ReadStateV1(jsonBytes []byte) (*stateV1, error) { + state := &stateV1{} + if err := json.Unmarshal(jsonBytes, state); err != nil { + return nil, fmt.Errorf("Decoding state file failed: %v", err) + } + + if state.Version != 1 { + return nil, fmt.Errorf("Decoded state version did not match the decoder selection: "+ + "read %d, expected 1", state.Version) + } + + return state, nil +} + +func ReadStateV2(jsonBytes []byte) (*State, error) { state := &State{} - if err := dec.Decode(state); err != nil { + if err := json.Unmarshal(jsonBytes, state); err != nil { return nil, fmt.Errorf("Decoding state file failed: %v", err) } @@ -1477,56 +1590,6 @@ func WriteState(d *State, dst io.Writer) error { return nil } -// upgradeV0State is used to upgrade a V0 state representation -// into a proper State representation. -func upgradeV0State(old *StateV0) (*State, error) { - s := &State{} - s.init() - - // Old format had no modules, so we migrate everything - // directly into the root module. - root := s.RootModule() - - // 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 { - newRs := &ResourceState{ - Type: rs.Type, - } - root.Resources[id] = newRs - - // Migrate to an instance state - instance := &InstanceState{ - ID: rs.ID, - Attributes: rs.Attributes, - } - - // Check if this is the primary or tainted instance - if _, ok := old.Tainted[id]; ok { - newRs.Tainted = append(newRs.Tainted, instance) - } else { - newRs.Primary = instance - } - - // Warn if the resource uses Extra, as there is - // no upgrade path for this! Now totally deprecated. - if len(rs.Extra) > 0 { - log.Printf( - "[WARN] Resource %s uses deprecated attribute "+ - "storage, state file upgrade may be incomplete.", - rs.ID, - ) - } - } - return s, nil -} - // moduleStateSort implements sort.Interface to sort module states type moduleStateSort []*ModuleState diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index 6d2f32696..b9fe35592 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -113,8 +113,12 @@ func TestStateAdd(t *testing.T) { "module.foo", &ModuleState{ Path: rootModulePath, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*OutputState{ + "foo": &OutputState{ + Type: "string", + Sensitive: false, + Value: "bar", + }, }, Dependencies: []string{"foo"}, Resources: map[string]*ResourceState{ @@ -139,8 +143,12 @@ func TestStateAdd(t *testing.T) { Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "foo"}, - Outputs: map[string]interface{}{ - "foo": "bar", + Outputs: map[string]*OutputState{ + "foo": &OutputState{ + Type: "string", + Sensitive: false, + Value: "bar", + }, }, Dependencies: []string{"foo"}, Resources: map[string]*ResourceState{ diff --git a/terraform/state_test.go b/terraform/state_test.go index 5c45c16e4..7a7dd0f0a 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/config" ) @@ -81,12 +82,10 @@ func TestStateOutputTypeRoundTrip(t *testing.T) { 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", + Outputs: map[string]*OutputState{ + "string_output": &OutputState{ + Value: "String Value", + Type: "string", }, }, }, @@ -1221,6 +1220,35 @@ func TestReadUpgradeStateV1toV2(t *testing.T) { } } +func TestReadUpgradeStateV1toV2_outputs(t *testing.T) { + // ReadState should transparently detect the old version but will upgrade + // it on Write. + actual, err := ReadState(strings.NewReader(testV1StateWithOutputs)) + 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) { + spew.Config.DisableMethods = true + t.Fatalf("bad:\n%s\n\nround tripped:\n%s\n", spew.Sdump(actual), spew.Sdump(roundTripped)) + spew.Config.DisableMethods = false + } +} + func TestReadUpgradeState(t *testing.T) { state := &StateV0{ Resources: map[string]*ResourceStateV0{ @@ -1241,7 +1269,7 @@ func TestReadUpgradeState(t *testing.T) { t.Fatalf("err: %s", err) } - upgraded, err := upgradeV0State(state) + upgraded, err := state.upgrade() if err != nil { t.Fatalf("err: %s", err) } @@ -1329,6 +1357,7 @@ func TestReadStateNewVersion(t *testing.T) { func TestReadStateTFVersion(t *testing.T) { type tfVersion struct { + Version int `json:"version"` TFVersion string `json:"terraform_version"` } @@ -1355,7 +1384,10 @@ func TestReadStateTFVersion(t *testing.T) { } for _, tc := range cases { - buf, err := json.Marshal(&tfVersion{tc.Written}) + buf, err := json.Marshal(&tfVersion{ + Version: 2, + TFVersion: tc.Written, + }) if err != nil { t.Fatalf("err: %v", err) } @@ -1443,7 +1475,7 @@ func TestUpgradeV0State(t *testing.T) { "bar": struct{}{}, }, } - state, err := upgradeV0State(old) + state, err := old.upgrade() if err != nil { t.Fatalf("err: %v", err) } @@ -1456,7 +1488,7 @@ func TestUpgradeV0State(t *testing.T) { if len(root.Outputs) != 1 { t.Fatalf("bad outputs: %v", root.Outputs) } - if root.Outputs["ip"] != "127.0.0.1" { + if root.Outputs["ip"].Value != "127.0.0.1" { t.Fatalf("bad outputs: %v", root.Outputs) } @@ -1588,3 +1620,37 @@ const testV1State = `{ ] } ` + +const testV1StateWithOutputs = `{ + "version": 1, + "serial": 9, + "remote": { + "type": "http", + "config": { + "url": "http://my-cool-server.com/" + } + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": { + "foo": "bar", + "baz": "foo" + }, + "resources": { + "foo": { + "type": "", + "primary": { + "id": "bar" + } + } + }, + "depends_on": [ + "aws_instance.bar" + ] + } + ] +} +` diff --git a/terraform/state_v0.go b/terraform/state_v0.go index 44f1664b8..d034c25a9 100644 --- a/terraform/state_v0.go +++ b/terraform/state_v0.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/hashicorp/terraform/config" + "log" ) // The format byte is prefixed into the state file format so that we have @@ -311,3 +312,57 @@ func ReadStateV0(src io.Reader) (*StateV0, error) { return result, nil } + +// upgradeV0State is used to upgrade a V0 state representation +// into a State (current) representation. +func (old *StateV0) upgrade() (*State, error) { + s := &State{} + s.init() + + // Old format had no modules, so we migrate everything + // directly into the root module. + root := s.RootModule() + + // Copy the outputs, first converting them to map[string]interface{} + oldOutputs := make(map[string]*OutputState, len(old.Outputs)) + for key, value := range old.Outputs { + oldOutputs[key] = &OutputState{ + Type: "string", + Sensitive: false, + Value: value, + } + } + root.Outputs = oldOutputs + + // Upgrade the resources + for id, rs := range old.Resources { + newRs := &ResourceState{ + Type: rs.Type, + } + root.Resources[id] = newRs + + // Migrate to an instance state + instance := &InstanceState{ + ID: rs.ID, + Attributes: rs.Attributes, + } + + // Check if this is the primary or tainted instance + if _, ok := old.Tainted[id]; ok { + newRs.Tainted = append(newRs.Tainted, instance) + } else { + newRs.Primary = instance + } + + // Warn if the resource uses Extra, as there is + // no upgrade path for this! Now totally deprecated. + if len(rs.Extra) > 0 { + log.Printf( + "[WARN] Resource %s uses deprecated attribute "+ + "storage, state file upgrade may be incomplete.", + rs.ID, + ) + } + } + return s, nil +} diff --git a/terraform/state_v1.go b/terraform/state_v1.go new file mode 100644 index 000000000..2872f8364 --- /dev/null +++ b/terraform/state_v1.go @@ -0,0 +1,334 @@ +package terraform + +import ( + "fmt" + + "github.com/mitchellh/copystructure" +) + +// stateV1 keeps track of a snapshot state-of-the-world that Terraform +// can use to keep track of what real world resources it is actually +// managing. +// +// stateV1 is _only used for the purposes of backwards compatibility +// and is no longer used in Terraform. +type stateV1 struct { + // Version is the protocol version. "1" for a StateV1. + Version int `json:"version"` + + // Serial is incremented on any operation that modifies + // the State file. It is used to detect potentially conflicting + // updates. + Serial int64 `json:"serial"` + + // Remote is used to track the metadata required to + // pull and push state files from a remote storage endpoint. + Remote *remoteStateV1 `json:"remote,omitempty"` + + // Modules contains all the modules in a breadth-first order + Modules []*moduleStateV1 `json:"modules"` +} + +// upgrade is used to upgrade a V1 state representation +// into a State (current) representation. +func (old *stateV1) upgrade() (*State, error) { + if old == nil { + return nil, nil + } + + remote, err := old.Remote.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading State V1: %v", err) + } + + modules := make([]*ModuleState, len(old.Modules)) + for i, module := range old.Modules { + upgraded, err := module.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading State V1: %v", err) + } + modules[i] = upgraded + } + if len(modules) == 0 { + modules = nil + } + + newState := &State{ + Version: old.Version, + Serial: old.Serial, + Remote: remote, + Modules: modules, + } + + newState.sort() + + return newState, nil +} + +type remoteStateV1 struct { + // Type controls the client we use for the remote state + Type string `json:"type"` + + // Config is used to store arbitrary configuration that + // is type specific + Config map[string]string `json:"config"` +} + +func (old *remoteStateV1) upgrade() (*RemoteState, error) { + if old == nil { + return nil, nil + } + + config, err := copystructure.Copy(old.Config) + if err != nil { + return nil, fmt.Errorf("Error upgrading RemoteState V1: %v", err) + } + + return &RemoteState{ + Type: old.Type, + Config: config.(map[string]string), + }, nil +} + +type moduleStateV1 struct { + // Path is the import path from the root module. Modules imports are + // always disjoint, so the path represents amodule tree + Path []string `json:"path"` + + // 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"` + + // Resources is a mapping of the logically named resource to + // the state of the resource. Each resource may actually have + // N instances underneath, although a user only needs to think + // about the 1:1 case. + Resources map[string]*resourceStateV1 `json:"resources"` + + // Dependencies are a list of things that this module relies on + // existing to remain intact. For example: an module may depend + // on a VPC ID given by an aws_vpc resource. + // + // Terraform uses this information to build valid destruction + // orders and to warn the user if they're destroying a module that + // another resource depends on. + // + // Things can be put into this list that may not be managed by + // Terraform. If Terraform doesn't find a matching ID in the + // overall state, then it assumes it isn't managed and doesn't + // worry about it. + Dependencies []string `json:"depends_on,omitempty"` +} + +func (old *moduleStateV1) upgrade() (*ModuleState, error) { + if old == nil { + return nil, nil + } + + path, err := copystructure.Copy(old.Path) + if err != nil { + return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) + } + + // Outputs needs upgrading to use the new structure + outputs := make(map[string]*OutputState) + for key, output := range old.Outputs { + outputs[key] = &OutputState{ + Type: "string", + Value: output, + Sensitive: false, + } + } + if len(outputs) == 0 { + outputs = nil + } + + resources := make(map[string]*ResourceState) + for key, oldResource := range old.Resources { + upgraded, err := oldResource.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) + } + resources[key] = upgraded + } + if len(resources) == 0 { + resources = nil + } + + dependencies, err := copystructure.Copy(old.Dependencies) + if err != nil { + return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) + } + + return &ModuleState{ + Path: path.([]string), + Outputs: outputs, + Resources: resources, + Dependencies: dependencies.([]string), + }, nil +} + +type resourceStateV1 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. + Type string `json:"type"` + + // Dependencies are a list of things that this resource relies on + // existing to remain intact. For example: an AWS instance might + // depend on a subnet (which itself might depend on a VPC, and so + // on). + // + // Terraform uses this information to build valid destruction + // orders and to warn the user if they're destroying a resource that + // another resource depends on. + // + // Things can be put into this list that may not be managed by + // Terraform. If Terraform doesn't find a matching ID in the + // overall state, then it assumes it isn't managed and doesn't + // worry about it. + Dependencies []string `json:"depends_on,omitempty"` + + // Primary is the current active instance for this resource. + // It can be replaced but only after a successful creation. + // This is the instances on which providers will act. + Primary *instanceStateV1 `json:"primary"` + + // Tainted is used to track any underlying instances that + // have been created but are in a bad or unknown state and + // need to be cleaned up subsequently. In the + // standard case, there is only at most a single instance. + // However, in pathological cases, it is possible for the number + // of instances to accumulate. + Tainted []*instanceStateV1 `json:"tainted,omitempty"` + + // Deposed is used in the mechanics of CreateBeforeDestroy: the existing + // Primary is Deposed to get it out of the way for the replacement Primary to + // be created by Apply. If the replacement Primary creates successfully, the + // Deposed instance is cleaned up. If there were problems creating the + // replacement, the instance remains in the Deposed list so it can be + // destroyed in a future run. Functionally, Deposed instances are very + // similar to Tainted instances in that Terraform is only tracking them in + // order to remember to destroy them. + Deposed []*instanceStateV1 `json:"deposed,omitempty"` + + // Provider is used when a resource is connected to a provider with an alias. + // If this string is empty, the resource is connected to the default provider, + // e.g. "aws_instance" goes with the "aws" provider. + // If the resource block contained a "provider" key, that value will be set here. + Provider string `json:"provider,omitempty"` +} + +func (old *resourceStateV1) upgrade() (*ResourceState, error) { + if old == nil { + return nil, nil + } + + dependencies, err := copystructure.Copy(old.Dependencies) + if err != nil { + return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) + } + + primary, err := old.Primary.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) + } + + tainted := make([]*InstanceState, len(old.Tainted)) + for i, v := range old.Tainted { + upgraded, err := v.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) + } + tainted[i] = upgraded + } + if len(tainted) == 0 { + tainted = nil + } + + deposed := make([]*InstanceState, len(old.Deposed)) + for i, v := range old.Deposed { + upgraded, err := v.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) + } + deposed[i] = upgraded + } + if len(deposed) == 0 { + deposed = nil + } + + return &ResourceState{ + Type: old.Type, + Dependencies: dependencies.([]string), + Primary: primary, + Tainted: tainted, + Deposed: deposed, + Provider: old.Provider, + }, nil +} + +type instanceStateV1 struct { + // A unique ID for this resource. This is opaque to Terraform + // and is only meant as a lookup mechanism for the providers. + ID string `json:"id"` + + // Attributes are basic information about the resource. Any keys here + // are accessible in variable format within Terraform configurations: + // ${resourcetype.name.attribute}. + Attributes map[string]string `json:"attributes,omitempty"` + + // Ephemeral is used to store any state associated with this instance + // that is necessary for the Terraform run to complete, but is not + // persisted to a state file. + Ephemeral ephemeralStateV1 `json:"-"` + + // Meta is a simple K/V map that is persisted to the State but otherwise + // ignored by Terraform core. It's meant to be used for accounting by + // external client code. + Meta map[string]string `json:"meta,omitempty"` +} + +func (old *instanceStateV1) upgrade() (*InstanceState, error) { + if old == nil { + return nil, nil + } + + attributes, err := copystructure.Copy(old.Attributes) + if err != nil { + return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) + } + ephemeral, err := old.Ephemeral.upgrade() + if err != nil { + return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) + } + meta, err := copystructure.Copy(old.Meta) + if err != nil { + return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) + } + + return &InstanceState{ + ID: old.ID, + Attributes: attributes.(map[string]string), + Ephemeral: *ephemeral, + Meta: meta.(map[string]string), + }, nil +} + +type ephemeralStateV1 struct { + // ConnInfo is used for the providers to export information which is + // used to connect to the resource for provisioning. For example, + // this could contain SSH or WinRM credentials. + ConnInfo map[string]string `json:"-"` +} + +func (old *ephemeralStateV1) upgrade() (*EphemeralState, error) { + connInfo, err := copystructure.Copy(old.ConnInfo) + if err != nil { + return nil, fmt.Errorf("Error upgrading EphemeralState V1: %v", err) + } + return &EphemeralState{ + ConnInfo: connInfo.(map[string]string), + }, nil +} diff --git a/terraform/transform_output_test.go b/terraform/transform_output_test.go index 6ba2150dc..3bbe7a3e8 100644 --- a/terraform/transform_output_test.go +++ b/terraform/transform_output_test.go @@ -11,9 +11,15 @@ func TestAddOutputOrphanTransformer(t *testing.T) { Modules: []*ModuleState{ &ModuleState{ Path: RootModulePath, - Outputs: map[string]interface{}{ - "foo": "bar", - "bar": "baz", + Outputs: map[string]*OutputState{ + "foo": &OutputState{ + Value: "bar", + Type: "string", + }, + "bar": &OutputState{ + Value: "baz", + Type: "string", + }, }, }, },