Merge pull request #7109 from hashicorp/f-state-lineage

core: State "Lineage" concept
This commit is contained in:
Paul Hinze 2016-06-10 16:54:31 -05:00 committed by GitHub
commit 00d004394c
2 changed files with 234 additions and 0 deletions

View File

@ -7,12 +7,15 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"reflect"
"sort"
"strconv"
"strings"
"github.com/hashicorp/go-version"
"github.com/satori/go.uuid"
"github.com/hashicorp/terraform/config"
"github.com/mitchellh/copystructure"
)
@ -62,6 +65,14 @@ type State struct {
// updates.
Serial int64 `json:"serial"`
// Lineage is set when a new, blank state is created and then
// never updated. This allows us to determine whether the serials
// of two states can be meaningfully compared.
// Apart from the guarantee that collisions between two lineages
// are very unlikely, this value is opaque and external callers
// should only compare lineage strings byte-for-byte for equality.
Lineage string `json:"lineage,omitempty"`
// Remote is used to track the metadata required to
// pull and push state files from a remote storage endpoint.
Remote *RemoteState `json:"remote,omitempty"`
@ -382,6 +393,68 @@ func (s *State) Equal(other *State) bool {
return true
}
type StateAgeComparison int
const (
StateAgeEqual StateAgeComparison = 0
StateAgeReceiverNewer StateAgeComparison = 1
StateAgeReceiverOlder StateAgeComparison = -1
)
// CompareAges compares one state with another for which is "older".
//
// This is a simple check using the state's serial, and is thus only as
// reliable as the serial itself. In the normal case, only one state
// exists for a given combination of lineage/serial, but Terraform
// does not guarantee this and so the result of this method should be
// used with care.
//
// Returns an integer that is negative if the receiver is older than
// the argument, positive if the converse, and zero if they are equal.
// An error is returned if the two states are not of the same lineage,
// in which case the integer returned has no meaning.
func (s *State) CompareAges(other *State) (StateAgeComparison, error) {
// nil states are "older" than actual states
switch {
case s != nil && other == nil:
return StateAgeReceiverNewer, nil
case s == nil && other != nil:
return StateAgeReceiverOlder, nil
case s == nil && other == nil:
return StateAgeEqual, nil
}
if !s.SameLineage(other) {
return StateAgeEqual, fmt.Errorf(
"can't compare two states of differing lineage",
)
}
switch {
case s.Serial < other.Serial:
return StateAgeReceiverOlder, nil
case s.Serial > other.Serial:
return StateAgeReceiverNewer, nil
default:
return StateAgeEqual, nil
}
}
// SameLineage returns true only if the state given in argument belongs
// to the same "lineage" of states as the reciever.
func (s *State) SameLineage(other *State) bool {
// If one of the states has no lineage then it is assumed to predate
// this concept, and so we'll accept it as belonging to any lineage
// so that a lineage string can be assigned to newer versions
// without breaking compatibility with older versions.
if s.Lineage == "" || other.Lineage == "" {
return true
}
return s.Lineage == other.Lineage
}
// DeepCopy performs a deep copy of the state structure and returns
// a new structure.
func (s *State) DeepCopy() *State {
@ -390,6 +463,7 @@ func (s *State) DeepCopy() *State {
}
n := &State{
Version: s.Version,
Lineage: s.Lineage,
TFVersion: s.TFVersion,
Serial: s.Serial,
Modules: make([]*ModuleState, 0, len(s.Modules)),
@ -443,6 +517,16 @@ func (s *State) init() {
if s.ModuleByPath(rootModulePath) == nil {
s.AddModule(rootModulePath)
}
s.EnsureHasLineage()
}
func (s *State) EnsureHasLineage() {
if s.Lineage == "" {
s.Lineage = uuid.NewV4().String()
log.Printf("[DEBUG] New state was assigned lineage %q\n", s.Lineage)
} else {
log.Printf("[TRACE] Preserving existing state lineage %q\n", s.Lineage)
}
}
// prune is used to remove any resources that are no longer required

View File

@ -338,6 +338,156 @@ func TestStateEqual(t *testing.T) {
}
}
func TestStateCompareAges(t *testing.T) {
cases := []struct {
Result StateAgeComparison
Err bool
One, Two *State
}{
{
StateAgeEqual, false,
&State{
Lineage: "1",
Serial: 2,
},
&State{
Lineage: "1",
Serial: 2,
},
},
{
StateAgeReceiverOlder, false,
&State{
Lineage: "1",
Serial: 2,
},
&State{
Lineage: "1",
Serial: 3,
},
},
{
StateAgeReceiverNewer, false,
&State{
Lineage: "1",
Serial: 3,
},
&State{
Lineage: "1",
Serial: 2,
},
},
{
StateAgeEqual, true,
&State{
Lineage: "1",
Serial: 2,
},
&State{
Lineage: "2",
Serial: 2,
},
},
{
StateAgeEqual, true,
&State{
Lineage: "1",
Serial: 3,
},
&State{
Lineage: "2",
Serial: 2,
},
},
}
for i, tc := range cases {
result, err := tc.One.CompareAges(tc.Two)
if err != nil && !tc.Err {
t.Errorf(
"%d: got error, but want success\n\n%s\n\n%s",
i, tc.One, tc.Two,
)
continue
}
if err == nil && tc.Err {
t.Errorf(
"%d: got success, but want error\n\n%s\n\n%s",
i, tc.One, tc.Two,
)
continue
}
if result != tc.Result {
t.Errorf(
"%d: got result %d, but want %d\n\n%s\n\n%s",
i, result, tc.Result, tc.One, tc.Two,
)
continue
}
}
}
func TestStateSameLineage(t *testing.T) {
cases := []struct {
Result bool
One, Two *State
}{
{
true,
&State{
Lineage: "1",
},
&State{
Lineage: "1",
},
},
{
// Empty lineage is compatible with all
true,
&State{
Lineage: "",
},
&State{
Lineage: "1",
},
},
{
// Empty lineage is compatible with all
true,
&State{
Lineage: "1",
},
&State{
Lineage: "",
},
},
{
false,
&State{
Lineage: "1",
},
&State{
Lineage: "2",
},
},
}
for i, tc := range cases {
result := tc.One.SameLineage(tc.Two)
if result != tc.Result {
t.Errorf(
"%d: got %v, but want %v\n\n%s\n\n%s",
i, result, tc.Result, tc.One, tc.Two,
)
continue
}
}
}
func TestStateIncrementSerialMaybe(t *testing.T) {
cases := map[string]struct {
S1, S2 *State