core: State "Lineage" concept

The lineage of a state is an identifier shared by a set of states whose
serials are meaningfully comparable because they are produced by
progressive Refresh/Apply operations from the same initial empty state.

This is initialized as a type-4 (random) UUID when a new state is
initialized and then preserved on all other changes.

Since states before this change will not have lineage but users may wish
to set a lineage for an existing state in order to get the safety
benefits it will grow to imply, an empty lineage is considered to be
compatible with all lineages.
This commit is contained in:
Martin Atkins 2016-05-08 13:02:49 -07:00
parent 19eb0079db
commit 985fa371dc
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