Skip to content

Commit cb0e20c

Browse files
author
Lee Trout
committed
Add support for force pushing with the remote backend
Both differing serials and lineage protections should be bypassed with the -force flag (in addition to resources). Compared to other backends we aren’t just shipping over the state bytes in a simple payload during the persistence phase of the push command and the force flag added to the Go TFE client needs to be specified at that time. To prevent changing every method signature of PersistState of the remote client I added an optional interface that provides a hook to flag the Client as operating in a force push context. Changing the method signature would be more explicit at the cost of not being used anywhere else currently or the optional interface pattern could be applied to the state itself so it could be upgraded to support PersistState(force bool) only when needed. Prior to this only the resources of the state were checked for changes not the lineage or the serial. To bring this in line with documented behavior noted above those attributes also have a “read” counterpart just like state has. These are now checked along with state to determine if the state as a whole is unchanged. Tests were altered to table driven test format and testing was expanded to include WriteStateForMigration and its interaction with a ClientForcePusher type.
1 parent c2c38b2 commit cb0e20c

File tree

5 files changed

+424
-99
lines changed

5 files changed

+424
-99
lines changed

backend/remote/backend_state.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type remoteClient struct {
2020
runID string
2121
stateUploadErr bool
2222
workspace *tfe.Workspace
23+
forcePush bool
2324
}
2425

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

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

102+
// EnableForcePush to allow the remote client to overwrite state
103+
// by implementing remote.ClientForcePusher
104+
func (r *remoteClient) EnableForcePush() {
105+
r.forcePush = true
106+
}
107+
100108
// Lock the remote state.
101109
func (r *remoteClient) Lock(info *state.LockInfo) (string, error) {
102110
ctx := context.Background()

state/remote/remote.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ type Client interface {
1515
Delete() error
1616
}
1717

18+
// ClientForcePusher is an optional interface that allows a remote
19+
// state to force push by managing a flag on the client that is
20+
// toggled on by a call to EnableForcePush.
21+
type ClientForcePusher interface {
22+
Client
23+
EnableForcePush()
24+
}
25+
1826
// ClientLocker is an optional interface that allows a remote state
1927
// backend to enable state lock/unlock.
2028
type ClientLocker interface {

state/remote/remote_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func (c nilClient) Delete() error { return nil }
6969
type mockClient struct {
7070
current []byte
7171
log []mockClientRequest
72+
force bool
7273
}
7374

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

9192
func (c *mockClient) Put(data []byte) error {
92-
c.appendLog("Put", data)
93+
if c.force {
94+
c.appendLog("Force Put", data)
95+
} else {
96+
c.appendLog("Put", data)
97+
}
9398
c.current = data
9499
return nil
95100
}
@@ -100,6 +105,11 @@ func (c *mockClient) Delete() error {
100105
return nil
101106
}
102107

108+
// Implements remote.ClientForcePusher
109+
func (c *mockClient) EnableForcePush() {
110+
c.force = true
111+
}
112+
103113
func (c *mockClient) appendLog(method string, content []byte) {
104114
// For easier test assertions, we actually log the result of decoding
105115
// the content JSON rather than the raw bytes. Callers are in principle

state/remote/state.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,19 @@ type State struct {
2121

2222
Client Client
2323

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

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

67-
checkFile := statefile.New(s.state, s.lineage, s.serial)
68-
if !force {
76+
// `force` is passed down from the CLI flag and terminates here. Actual
77+
// force pushing with the remote backend happens when Put()'ing the contents
78+
// in the backend. If force is specified we skip verifications and hand the
79+
// context off to the client to use when persitence operations actually take place.
80+
c, isForcePusher := s.Client.(ClientForcePusher)
81+
if force && isForcePusher {
82+
c.EnableForcePush()
83+
} else {
84+
checkFile := statefile.New(s.state, s.lineage, s.serial)
6985
if err := statemgr.CheckValidImport(f, checkFile); err != nil {
7086
return err
7187
}
@@ -113,7 +129,12 @@ func (s *State) refreshState() error {
113129
s.lineage = stateFile.Lineage
114130
s.serial = stateFile.Serial
115131
s.state = stateFile.State
116-
s.readState = s.state.DeepCopy() // our states must be separate instances so we can track changes
132+
133+
// Properties from the remote must be separate so we can
134+
// track changes as lineage, serial and/or state are mutated
135+
s.readLineage = stateFile.Lineage
136+
s.readSerial = stateFile.Serial
137+
s.readState = s.state.DeepCopy()
117138
return nil
118139
}
119140

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

125146
if s.readState != nil {
126-
if statefile.StatesMarshalEqual(s.state, s.readState) {
127-
// If the state hasn't changed at all then we have nothing to do.
147+
lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage
148+
serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial
149+
stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState)
150+
if stateUnchanged && lineageUnchanged && serialUnchanged {
151+
// If the state, lineage or serial haven't changed at all then we have nothing to do.
128152
return nil
129153
}
130154
s.serial++
@@ -161,7 +185,13 @@ func (s *State) PersistState() error {
161185

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

0 commit comments

Comments
 (0)