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.
This commit is contained in:
James Nugent 2016-06-07 20:04:01 +02:00
parent 91587a49f3
commit 49995428fd
4 changed files with 10 additions and 591 deletions

View File

@ -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.

View File

@ -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

View File

@ -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 "<no state>"
}
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 = "<not created>"
}
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
}

View File

@ -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
}