Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/remote/backend_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type remoteClient struct {
runID string
stateUploadErr bool
workspace *tfe.Workspace
forcePush bool
}

// Get the remote state.
Expand Down Expand Up @@ -69,6 +70,7 @@ func (r *remoteClient) Put(state []byte) error {
Serial: tfe.Int64(int64(stateFile.Serial)),
MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
State: tfe.String(base64.StdEncoding.EncodeToString(state)),
Force: tfe.Bool(r.forcePush),
}

// If we have a run ID, make sure to add it to the options
Expand Down Expand Up @@ -97,6 +99,12 @@ func (r *remoteClient) Delete() error {
return nil
}

// EnableForcePush to allow the remote client to overwrite state
// by implementing remote.ClientForcePusher
func (r *remoteClient) EnableForcePush() {
r.forcePush = true
}

// Lock the remote state.
func (r *remoteClient) Lock(info *state.LockInfo) (string, error) {
ctx := context.Background()
Expand Down
8 changes: 8 additions & 0 deletions state/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ type Client interface {
Delete() error
}

// ClientForcePusher is an optional interface that allows a remote
// state to force push by managing a flag on the client that is
// toggled on by a call to EnableForcePush.
type ClientForcePusher interface {
Client
EnableForcePush()
}

// ClientLocker is an optional interface that allows a remote state
// backend to enable state lock/unlock.
type ClientLocker interface {
Expand Down
12 changes: 11 additions & 1 deletion state/remote/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (c nilClient) Delete() error { return nil }
type mockClient struct {
current []byte
log []mockClientRequest
force bool
}

type mockClientRequest struct {
Expand All @@ -89,7 +90,11 @@ func (c *mockClient) Get() (*Payload, error) {
}

func (c *mockClient) Put(data []byte) error {
c.appendLog("Put", data)
if c.force {
c.appendLog("Force Put", data)
} else {
c.appendLog("Put", data)
}
c.current = data
return nil
}
Expand All @@ -100,6 +105,11 @@ func (c *mockClient) Delete() error {
return nil
}

// Implements remote.ClientForcePusher
func (c *mockClient) EnableForcePush() {
c.force = true
}

func (c *mockClient) appendLog(method string, content []byte) {
// For easier test assertions, we actually log the result of decoding
// the content JSON rather than the raw bytes. Callers are in principle
Expand Down
48 changes: 39 additions & 9 deletions state/remote/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ type State struct {

Client Client

lineage string
serial uint64
state, readState *states.State
disableLocks bool
// We track two pieces of meta data in addition to the state itself:
//
// lineage - the state's unique ID
// serial - the monotonic counter of "versions" of the state
//
// Both of these (along with state) have a sister field
// that represents the values read in from an existing source.
// All three of these values are used to determine if the new
// state has changed from an existing state we read in.
lineage, readLineage string
serial, readSerial uint64
state, readState *states.State
disableLocks bool
}

var _ statemgr.Full = (*State)(nil)
Expand Down Expand Up @@ -64,8 +73,15 @@ func (s *State) WriteStateForMigration(f *statefile.File, force bool) error {
s.mu.Lock()
defer s.mu.Unlock()

checkFile := statefile.New(s.state, s.lineage, s.serial)
if !force {
// `force` is passed down from the CLI flag and terminates here. Actual
// force pushing with the remote backend happens when Put()'ing the contents
// in the backend. If force is specified we skip verifications and hand the
// context off to the client to use when persitence operations actually take place.
c, isForcePusher := s.Client.(ClientForcePusher)
if force && isForcePusher {
c.EnableForcePush()
} else {
checkFile := statefile.New(s.state, s.lineage, s.serial)
if err := statemgr.CheckValidImport(f, checkFile); err != nil {
return err
}
Expand Down Expand Up @@ -113,7 +129,12 @@ func (s *State) refreshState() error {
s.lineage = stateFile.Lineage
s.serial = stateFile.Serial
s.state = stateFile.State
s.readState = s.state.DeepCopy() // our states must be separate instances so we can track changes

// Properties from the remote must be separate so we can
// track changes as lineage, serial and/or state are mutated
s.readLineage = stateFile.Lineage
s.readSerial = stateFile.Serial
s.readState = s.state.DeepCopy()
return nil
}

Expand All @@ -123,8 +144,11 @@ func (s *State) PersistState() error {
defer s.mu.Unlock()

if s.readState != nil {
if statefile.StatesMarshalEqual(s.state, s.readState) {
// If the state hasn't changed at all then we have nothing to do.
lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage
serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial
stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState)
if stateUnchanged && lineageUnchanged && serialUnchanged {
// If the state, lineage or serial haven't changed at all then we have nothing to do.
return nil
}
s.serial++
Expand Down Expand Up @@ -161,7 +185,13 @@ func (s *State) PersistState() error {

// After we've successfully persisted, what we just wrote is our new
// reference state until someone calls RefreshState again.
// We've potentially overwritten (via force) the state, lineage
// and / or serial (and serial was incremented) so we copy over all
// three fields so everything matches the new state and a subsequent
// operation would correctly detect no changes to the lineage, serial or state.
s.readState = s.state.DeepCopy()
s.readLineage = s.lineage
s.readSerial = s.serial
return nil
}

Expand Down
Loading