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:
parent
91587a49f3
commit
49995428fd
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue