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:
parent
19eb0079db
commit
985fa371dc
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue