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"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/mitchellh/copystructure"
|
"github.com/mitchellh/copystructure"
|
||||||
)
|
)
|
||||||
|
@ -62,6 +65,14 @@ type State struct {
|
||||||
// updates.
|
// updates.
|
||||||
Serial int64 `json:"serial"`
|
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
|
// Remote is used to track the metadata required to
|
||||||
// pull and push state files from a remote storage endpoint.
|
// pull and push state files from a remote storage endpoint.
|
||||||
Remote *RemoteState `json:"remote,omitempty"`
|
Remote *RemoteState `json:"remote,omitempty"`
|
||||||
|
@ -382,6 +393,68 @@ func (s *State) Equal(other *State) bool {
|
||||||
return true
|
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
|
// DeepCopy performs a deep copy of the state structure and returns
|
||||||
// a new structure.
|
// a new structure.
|
||||||
func (s *State) DeepCopy() *State {
|
func (s *State) DeepCopy() *State {
|
||||||
|
@ -390,6 +463,7 @@ func (s *State) DeepCopy() *State {
|
||||||
}
|
}
|
||||||
n := &State{
|
n := &State{
|
||||||
Version: s.Version,
|
Version: s.Version,
|
||||||
|
Lineage: s.Lineage,
|
||||||
TFVersion: s.TFVersion,
|
TFVersion: s.TFVersion,
|
||||||
Serial: s.Serial,
|
Serial: s.Serial,
|
||||||
Modules: make([]*ModuleState, 0, len(s.Modules)),
|
Modules: make([]*ModuleState, 0, len(s.Modules)),
|
||||||
|
@ -443,6 +517,16 @@ func (s *State) init() {
|
||||||
if s.ModuleByPath(rootModulePath) == nil {
|
if s.ModuleByPath(rootModulePath) == nil {
|
||||||
s.AddModule(rootModulePath)
|
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
|
// 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) {
|
func TestStateIncrementSerialMaybe(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
S1, S2 *State
|
S1, S2 *State
|
||||||
|
|
Loading…
Reference in New Issue