From 49995428fdb23583a0711d888547a6c53dbf2280 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Tue, 7 Jun 2016 20:04:01 +0200 Subject: [PATCH] core: Remove support for V0 state This removes support for the V0 binary state format which was present in Terraform prior to 0.3. We still check for the file type and present an error message explaining to the user that they can upgrade it using a prior version of Terraform. --- terraform/state.go | 19 +- terraform/state_test.go | 97 ---------- terraform/state_v0.go | 367 ------------------------------------- terraform/state_v0_test.go | 118 ------------ 4 files changed, 10 insertions(+), 591 deletions(-) delete mode 100644 terraform/state_v0.go delete mode 100644 terraform/state_v0_test.go diff --git a/terraform/state.go b/terraform/state.go index 02ee9cf4f..63dbe95ef 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -1384,18 +1384,19 @@ type jsonStateVersionIdentifier struct { func ReadState(src io.Reader) (*State, error) { buf := bufio.NewReader(src) - // Check if this is a V0 format - start, err := buf.Peek(len(stateFormatMagic)) + // Check if this is a V0 format - the magic bytes at the start of the file + // should be "tfstate" if so. We no longer support upgrading this type of + // state but display an error message explaining to a user how they can + // upgrade via the 0.6.x series. + start, err := buf.Peek(len("tfstate")) if err != nil { return nil, fmt.Errorf("Failed to check for magic bytes: %v", err) } - if string(start) == stateFormatMagic { - // Read the old state - old, err := ReadStateV0(buf) - if err != nil { - return nil, err - } - return old.upgrade() + if string(start) == "tfstate" { + return nil, fmt.Errorf("Terraform 0.7 no longer supports upgrading the binary state\n" + + "format which was used prior to Terraform 0.3. Please upgrade\n" + + "this state file using Terraform 0.6.16 prior to using it with\n" + + "Terraform 0.7.") } // If we are JSON we buffer the whole thing in memory so we can read it twice. diff --git a/terraform/state_test.go b/terraform/state_test.go index e3ee69137..f265074aa 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -1184,36 +1184,6 @@ func TestReadUpgradeStateV1toV2_outputs(t *testing.T) { } } -func TestReadUpgradeState(t *testing.T) { - state := &StateV0{ - Resources: map[string]*ResourceStateV0{ - "foo": &ResourceStateV0{ - ID: "bar", - }, - }, - } - buf := new(bytes.Buffer) - if err := testWriteStateV0(state, buf); err != nil { - t.Fatalf("err: %s", err) - } - - // ReadState should transparently detect the old - // version and upgrade up so the latest. - actual, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - upgraded, err := state.upgrade() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(actual, upgraded) { - t.Fatalf("bad: %#v", actual) - } -} - func TestReadWriteState(t *testing.T) { state := &State{ Serial: 9, @@ -1385,73 +1355,6 @@ func TestWriteStateTFVersion(t *testing.T) { } } -func TestUpgradeV0State(t *testing.T) { - old := &StateV0{ - Outputs: map[string]string{ - "ip": "127.0.0.1", - }, - Resources: map[string]*ResourceStateV0{ - "foo": &ResourceStateV0{ - Type: "test_resource", - ID: "bar", - Attributes: map[string]string{ - "key": "val", - }, - }, - "bar": &ResourceStateV0{ - Type: "test_resource", - ID: "1234", - Attributes: map[string]string{ - "a": "b", - }, - }, - }, - Tainted: map[string]struct{}{ - "bar": struct{}{}, - }, - } - state, err := old.upgrade() - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(state.Modules) != 1 { - t.Fatalf("should only have root module: %#v", state.Modules) - } - root := state.RootModule() - - if len(root.Outputs) != 1 { - t.Fatalf("bad outputs: %v", root.Outputs) - } - if root.Outputs["ip"].Value != "127.0.0.1" { - t.Fatalf("bad outputs: %v", root.Outputs) - } - - if len(root.Resources) != 2 { - t.Fatalf("bad resources: %v", root.Resources) - } - - foo := root.Resources["foo"] - if foo.Type != "test_resource" { - t.Fatalf("bad: %#v", foo) - } - if foo.Primary == nil || foo.Primary.ID != "bar" || - foo.Primary.Attributes["key"] != "val" { - t.Fatalf("bad: %#v", foo) - } - if foo.Primary.Tainted { - t.Fatalf("bad: %#v", foo) - } - - bar := root.Resources["bar"] - if bar.Type != "test_resource" { - t.Fatalf("bad: %#v", bar) - } - if !bar.Primary.Tainted { - t.Fatalf("bad: %#v", bar) - } -} - func TestParseResourceStateKey(t *testing.T) { cases := []struct { Input string diff --git a/terraform/state_v0.go b/terraform/state_v0.go deleted file mode 100644 index 33bc9b238..000000000 --- a/terraform/state_v0.go +++ /dev/null @@ -1,367 +0,0 @@ -package terraform - -import ( - "bytes" - "encoding/gob" - "errors" - "fmt" - "io" - "sort" - "strings" - "sync" - - "log" - - "github.com/hashicorp/terraform/config" -) - -// The format byte is prefixed into the state file format so that we have -// the ability in the future to change the file format if we want for any -// reason. -const ( - stateFormatMagic = "tfstate" - stateFormatVersion byte = 1 -) - -// StateV0 is used to represent the state of Terraform files before -// 0.3. It is automatically upgraded to a modern State representation -// on start. -type StateV0 struct { - Outputs map[string]string - Resources map[string]*ResourceStateV0 - Tainted map[string]struct{} - - once sync.Once -} - -func (s *StateV0) init() { - s.once.Do(func() { - if s.Resources == nil { - s.Resources = make(map[string]*ResourceStateV0) - } - - if s.Tainted == nil { - s.Tainted = make(map[string]struct{}) - } - }) -} - -func (s *StateV0) deepcopy() *StateV0 { - result := new(StateV0) - result.init() - if s != nil { - for k, v := range s.Resources { - result.Resources[k] = v - } - for k, v := range s.Tainted { - result.Tainted[k] = v - } - } - - return result -} - -// prune is a helper that removes any empty IDs from the state -// and cleans it up in general. -func (s *StateV0) prune() { - for k, v := range s.Resources { - if v.ID == "" { - delete(s.Resources, k) - } - } -} - -// Orphans returns a list of keys of resources that are in the State -// but aren't present in the configuration itself. Hence, these keys -// represent the state of resources that are orphans. -func (s *StateV0) Orphans(c *config.Config) []string { - keys := make(map[string]struct{}) - for k, _ := range s.Resources { - keys[k] = struct{}{} - } - - for _, r := range c.Resources { - delete(keys, r.Id()) - - for k, _ := range keys { - if strings.HasPrefix(k, r.Id()+".") { - delete(keys, k) - } - } - } - - result := make([]string, 0, len(keys)) - for k, _ := range keys { - result = append(result, k) - } - - return result -} - -func (s *StateV0) String() string { - if len(s.Resources) == 0 { - return "" - } - - var buf bytes.Buffer - - names := make([]string, 0, len(s.Resources)) - for name, _ := range s.Resources { - names = append(names, name) - } - sort.Strings(names) - - for _, k := range names { - rs := s.Resources[k] - id := rs.ID - if id == "" { - id = "" - } - - taintStr := "" - if _, ok := s.Tainted[k]; ok { - taintStr = " (tainted)" - } - - buf.WriteString(fmt.Sprintf("%s:%s\n", k, taintStr)) - buf.WriteString(fmt.Sprintf(" ID = %s\n", id)) - - attrKeys := make([]string, 0, len(rs.Attributes)) - for ak, _ := range rs.Attributes { - if ak == "id" { - continue - } - - attrKeys = append(attrKeys, ak) - } - sort.Strings(attrKeys) - - for _, ak := range attrKeys { - av := rs.Attributes[ak] - buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) - } - - if len(rs.Dependencies) > 0 { - buf.WriteString(fmt.Sprintf("\n Dependencies:\n")) - for _, dep := range rs.Dependencies { - buf.WriteString(fmt.Sprintf(" %s\n", dep.ID)) - } - } - } - - if len(s.Outputs) > 0 { - buf.WriteString("\nOutputs:\n\n") - - ks := make([]string, 0, len(s.Outputs)) - for k, _ := range s.Outputs { - ks = append(ks, k) - } - sort.Strings(ks) - - for _, k := range ks { - v := s.Outputs[k] - buf.WriteString(fmt.Sprintf("%s = %s\n", k, v)) - } - } - - return buf.String() -} - -/// ResourceState holds the state of a resource that is used so that -// a provider can find and manage an existing resource as well as for -// storing attributes that are uesd to populate variables of child -// resources. -// -// Attributes has attributes about the created resource that are -// queryable in interpolation: "${type.id.attr}" -// -// Extra is just extra data that a provider can return that we store -// for later, but is not exposed in any way to the user. -type ResourceStateV0 struct { - // This is filled in and managed by Terraform, and is the resource - // type itself such as "mycloud_instance". If a resource provider sets - // this value, it won't be persisted. - Type string - - // The attributes below are all meant to be filled in by the - // resource providers themselves. Documentation for each are above - // each element. - - // A unique ID for this resource. This is opaque to Terraform - // and is only meant as a lookup mechanism for the providers. - ID string - - // 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 - - // 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 - - // Extra information that the provider can store about a resource. - // This data is opaque, never shown to the user, and is sent back to - // the provider as-is for whatever purpose appropriate. - Extra map[string]interface{} - - // 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 []ResourceDependency -} - -// MergeDiff takes a ResourceDiff and merges the attributes into -// this resource state in order to generate a new state. This new -// state can be used to provide updated attribute lookups for -// variable interpolation. -// -// If the diff attribute requires computing the value, and hence -// won't be available until apply, the value is replaced with the -// computeID. -func (s *ResourceStateV0) MergeDiff(d *InstanceDiff) *ResourceStateV0 { - var result ResourceStateV0 - if s != nil { - result = *s - } - - result.Attributes = make(map[string]string) - if s != nil { - for k, v := range s.Attributes { - result.Attributes[k] = v - } - } - if d != nil { - for k, diff := range d.Attributes { - if diff.NewRemoved { - delete(result.Attributes, k) - continue - } - if diff.NewComputed { - result.Attributes[k] = config.UnknownVariableValue - continue - } - - result.Attributes[k] = diff.New - } - } - - return &result -} - -func (s *ResourceStateV0) GoString() string { - return fmt.Sprintf("*%#v", *s) -} - -// ResourceDependency maps a resource to another resource that it -// depends on to remain intact and uncorrupted. -type ResourceDependency struct { - // ID of the resource that we depend on. This ID should map - // directly to another ResourceState's ID. - ID string -} - -// ReadStateV0 reads a state structure out of a reader in the format that -// was written by WriteState. -func ReadStateV0(src io.Reader) (*StateV0, error) { - var result *StateV0 - var err error - n := 0 - - // Verify the magic bytes - magic := make([]byte, len(stateFormatMagic)) - for n < len(magic) { - n, err = src.Read(magic[n:]) - if err != nil { - return nil, fmt.Errorf("error while reading magic bytes: %s", err) - } - } - if string(magic) != stateFormatMagic { - return nil, fmt.Errorf("not a valid state file") - } - - // Verify the version is something we can read - var formatByte [1]byte - n, err = src.Read(formatByte[:]) - if err != nil { - return nil, err - } - if n != len(formatByte) { - return nil, errors.New("failed to read state version byte") - } - - if formatByte[0] != stateFormatVersion { - return nil, fmt.Errorf("unknown state file version: %d", formatByte[0]) - } - - // Decode - dec := gob.NewDecoder(src) - if err := dec.Decode(&result); err != nil { - return nil, err - } - - 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 - newRs.Primary = &InstanceState{ - ID: rs.ID, - Attributes: rs.Attributes, - } - - // Check if old resource was tainted - if _, ok := old.Tainted[id]; ok { - newRs.Primary.Tainted = true - } - - // 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_v0_test.go b/terraform/state_v0_test.go deleted file mode 100644 index 04f84545c..000000000 --- a/terraform/state_v0_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package terraform - -import ( - "bytes" - "encoding/gob" - "errors" - "io" - "reflect" - "sync" - "testing" - - "github.com/mitchellh/hashstructure" -) - -func TestReadWriteStateV0(t *testing.T) { - state := &StateV0{ - Resources: map[string]*ResourceStateV0{ - "foo": &ResourceStateV0{ - ID: "bar", - ConnInfo: map[string]string{ - "type": "ssh", - "user": "root", - "password": "supersecret", - }, - }, - }, - } - - // Checksum before the write - chksum, err := hashstructure.Hash(state, nil) - if err != nil { - t.Fatalf("hash: %s", err) - } - - buf := new(bytes.Buffer) - if err := testWriteStateV0(state, buf); err != nil { - t.Fatalf("err: %s", err) - } - - // Checksum after the write - chksumAfter, err := hashstructure.Hash(state, nil) - if err != nil { - t.Fatalf("hash: %s", err) - } - - if chksumAfter != chksum { - t.Fatalf("structure changed during serialization!") - } - - actual, err := ReadStateV0(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - // ReadState should not restore sensitive information! - state.Resources["foo"].ConnInfo = nil - - if !reflect.DeepEqual(actual, state) { - t.Fatalf("bad: %#v", actual) - } -} - -// sensitiveState is used to store sensitive state information -// that should not be serialized. This is only used temporarily -// and is restored into the state. -type sensitiveState struct { - ConnInfo map[string]map[string]string - - once sync.Once -} - -func (s *sensitiveState) init() { - s.once.Do(func() { - s.ConnInfo = make(map[string]map[string]string) - }) -} - -// testWriteStateV0 writes a state somewhere in a binary format. -// Only for testing now -func testWriteStateV0(d *StateV0, dst io.Writer) error { - // Write the magic bytes so we can determine the file format later - n, err := dst.Write([]byte(stateFormatMagic)) - if err != nil { - return err - } - if n != len(stateFormatMagic) { - return errors.New("failed to write state format magic bytes") - } - - // Write a version byte so we can iterate on version at some point - n, err = dst.Write([]byte{stateFormatVersion}) - if err != nil { - return err - } - if n != 1 { - return errors.New("failed to write state version byte") - } - - // Prevent sensitive information from being serialized - sensitive := &sensitiveState{} - sensitive.init() - for name, r := range d.Resources { - if r.ConnInfo != nil { - sensitive.ConnInfo[name] = r.ConnInfo - r.ConnInfo = nil - } - } - - // Serialize the state - err = gob.NewEncoder(dst).Encode(d) - - // Restore the state - for name, info := range sensitive.ConnInfo { - d.Resources[name].ConnInfo = info - } - - return err -}