diff --git a/terraform/state.go b/terraform/state.go index f1e0ef7d9a..5bef38f660 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -14,6 +14,7 @@ import ( "strings" "sync" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/config" "github.com/mitchellh/copystructure" @@ -247,6 +248,46 @@ func (s *State) IsRemote() bool { return true } +// Validate validates the integrity of this state file. +// +// Certain properties of the statefile are expected by Terraform in order +// to behave properly. The core of Terraform will assume that once it +// receives a State structure that it has been validated. This validation +// check should be called to ensure that. +// +// If this returns an error, then the user should be notified. The error +// response will include detailed information on the nature of the error. +func (s *State) Validate() error { + var result error + + // !!!! FOR DEVELOPERS !!!! + // + // Any errors returned from this Validate function will BLOCK TERRAFORM + // from loading a state file. Therefore, this should only contain checks + // that are only resolvable through manual intervention. + // + // !!!! FOR DEVELOPERS !!!! + + // Make sure there are no duplicate module states. We open a new + // block here so we can use basic variable names and future validations + // can do the same. + { + found := make(map[string]struct{}) + for _, ms := range s.Modules { + key := strings.Join(ms.Path, ".") + if _, ok := found[key]; ok { + result = multierror.Append(result, fmt.Errorf( + strings.TrimSpace(stateValidateErrMultiModule), key)) + continue + } + + found[key] = struct{}{} + } + } + + return result +} + // Remove removes the item in the state at the given address, returning // any errors that may have occurred. // @@ -1549,6 +1590,7 @@ func ReadState(src io.Reader) (*State, error) { return nil, fmt.Errorf("Decoding state file version failed: %v", err) } + var result *State switch versionIdentifier.Version { case 0: return nil, fmt.Errorf("State version 0 is not supported as JSON.") @@ -1570,7 +1612,7 @@ func ReadState(src io.Reader) (*State, error) { // increment the Serial whenever we upgrade state v3State.Serial++ - return v3State, nil + result = v3State case 2: v2State, err := ReadStateV2(jsonBytes) if err != nil { @@ -1582,18 +1624,30 @@ func ReadState(src io.Reader) (*State, error) { } v3State.Serial++ - return v3State, nil + result = v3State case 3: v3State, err := ReadStateV3(jsonBytes) if err != nil { return nil, err } - return v3State, nil + + result = v3State default: return nil, fmt.Errorf("Terraform %s does not support state version %d, please update.", SemVersion.String(), versionIdentifier.Version) } + // If we reached this place we must have a result set + if result == nil { + panic("resulting state in load not set, assertion failed") + } + + // Validate the state file is valid + if err := result.Validate(); err != nil { + return nil, err + } + + return result, nil } func ReadStateV1(jsonBytes []byte) (*stateV1, error) { @@ -1758,3 +1812,12 @@ func (s moduleStateSort) Less(i, j int) bool { func (s moduleStateSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +const stateValidateErrMultiModule = ` +Multiple modules with the same path: %s + +This means that there are multiple entries in the "modules" field +in your state file that point to the same module. This will cause Terraform +to behave in unexpected and error prone ways and is invalid. Please back up +and modify your state file manually to resolve this. +` diff --git a/terraform/state_test.go b/terraform/state_test.go index 34b2c362c2..b5a23fafb8 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -10,6 +10,42 @@ import ( "github.com/hashicorp/terraform/config" ) +func TestStateValidate(t *testing.T) { + cases := map[string]struct { + In *State + Err bool + }{ + "empty state": { + &State{}, + false, + }, + + "multiple modules": { + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + }, + &ModuleState{ + Path: []string{"root", "foo"}, + }, + }, + }, + true, + }, + } + + for name, tc := range cases { + // Init the state + tc.In.init() + + err := tc.In.Validate() + if (err != nil) != tc.Err { + t.Fatalf("%s: err: %s", name, err) + } + } +} + func TestStateAddModule(t *testing.T) { cases := []struct { In [][]string