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", + }, }, }, },