Merge pull request #7109 from hashicorp/f-state-lineage
core: State "Lineage" concept
This commit is contained in:
commit
00d004394c
|
@ -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