@@ -2,8 +2,10 @@ package remote
22
33import (
44 "bytes"
5+ "crypto/md5"
56 "fmt"
67 "io"
8+ "io/ioutil"
79 "net/url"
810 "os"
911 "path/filepath"
@@ -29,6 +31,82 @@ const (
2931 DefaultServer = "http://www.hashicorp.com/"
3032)
3133
34+ // StateChangeResult is used to communicate to a caller
35+ // what actions have been taken when updating a state file
36+ type StateChangeResult int
37+
38+ const (
39+ // StateChangeNoop indicates nothing has happened,
40+ // but that does not indicate an error. Everything is
41+ // just up to date. (Push/Pull)
42+ StateChangeNoop StateChangeResult = iota
43+
44+ // StateChangeUpdateLocal indicates the local state
45+ // was updated. (Pull)
46+ StateChangeUpdateLocal
47+
48+ // StateChangeUpdateRemote indicates the remote state
49+ // was updated. (Push)
50+ StateChangeUpdateRemote
51+
52+ // StateChangeLocalNewer means the pull was a no-op
53+ // because the local state is newer than that of the
54+ // server. This means a Push should take place. (Pull)
55+ StateChangeLocalNewer
56+
57+ // StateChangeRemoteNewer means the push was a no-op
58+ // because the remote state is newer than that of the
59+ // local state. This means a Pull should take place.
60+ // (Push)
61+ StateChangeRemoteNewer
62+
63+ // StateChangeConflict means that the push or pull
64+ // was a no-op because there is a conflict. This means
65+ // there are multiple state definitions at the same
66+ // serial number with different contents. This requires
67+ // an operator to intervene and resolve the conflict.
68+ // Shame on the user for doing concurrent apply.
69+ // (Push/Pull)
70+ StateChangeConflict
71+ )
72+
73+ func (sc StateChangeResult ) String () string {
74+ switch sc {
75+ case StateChangeNoop :
76+ return "Local and remote state in sync"
77+ case StateChangeUpdateLocal :
78+ return "Local state updated"
79+ case StateChangeUpdateRemote :
80+ return "Remote state updated"
81+ case StateChangeLocalNewer :
82+ return "Local state is newer than remote state, push required"
83+ case StateChangeRemoteNewer :
84+ return "Remote state is newer than local state, pull required"
85+ case StateChangeConflict :
86+ return "Local and remote state conflict, manual resolution required"
87+ default :
88+ return fmt .Sprintf ("Unknown state change type: %d" , sc )
89+ }
90+ }
91+
92+ // SuccessfulPull is used to clasify the StateChangeResult for
93+ // a pull operation. This is different by operation, but can be used
94+ // to determine a proper exit code.
95+ func (sc StateChangeResult ) SuccessfulPull () bool {
96+ switch sc {
97+ case StateChangeNoop :
98+ return true
99+ case StateChangeUpdateLocal :
100+ return true
101+ case StateChangeLocalNewer :
102+ return false
103+ case StateChangeConflict :
104+ return false
105+ default :
106+ return false
107+ }
108+ }
109+
32110// EnsureDirectory is used to make sure the local storage
33111// directory exists
34112func EnsureDirectory () error {
@@ -126,26 +204,113 @@ This is likely a bug, please report it.`)
126204 return nil
127205}
128206
129- // ReadState is used to read the remote state given
130- // the configuration for the remote endpoint. We return
131- // a boolean indicating if the remote state exists, along
132- // with the state, and possible error.
133- func ReadState (conf * terraform.RemoteState ) (io.Reader , error ) {
134- // TODO: Read actually from a server
207+ // RefreshState is used to read the remote state given
208+ // the configuration for the remote endpoint, and update
209+ // the local state if necessary.
210+ func RefreshState (conf * terraform.RemoteState ) (StateChangeResult , error ) {
211+ // Read the state from the server
212+ payload , err := GetState (conf )
213+ if err != nil {
214+ return StateChangeNoop ,
215+ fmt .Errorf ("Failed to read remote state: %v" , err )
216+ }
217+
218+ // Parse the remote state
219+ var remoteState * terraform.State
220+ if payload != nil {
221+ remoteState , err = terraform .ReadState (bytes .NewReader (payload .State ))
222+ if err != nil {
223+ return StateChangeNoop ,
224+ fmt .Errorf ("Failed to parse remote state: %v" , err )
225+ }
226+
227+ // Ensure we understand the remote version!
228+ if remoteState .Version > terraform .StateVersion {
229+ return StateChangeNoop , fmt .Errorf (
230+ `Remote state is version %d, this version of Terraform only understands up to %d` , remoteState .Version , terraform .StateVersion )
231+ }
232+ }
233+
234+ // Get the path to the state file
235+ path , err := HiddenStatePath ()
236+ if err != nil {
237+ return StateChangeNoop , err
238+ }
239+
240+ // Get the existing state file
241+ raw , err := ioutil .ReadFile (path )
242+ if err != nil && ! os .IsNotExist (err ) {
243+ return StateChangeNoop , fmt .Errorf ("Failed to read local state: %v" , err )
244+ }
245+
246+ // Decode the state
247+ var localState * terraform.State
248+ if raw != nil {
249+ localState , err = terraform .ReadState (bytes .NewReader (raw ))
250+ if err != nil {
251+ return StateChangeNoop ,
252+ fmt .Errorf ("Failed to decode state file '%s': %v" , path , err )
253+ }
254+ }
255+
256+ // We need to handle the matrix of cases in reconciling
257+ // the local and remote state. Primarily the concern is
258+ // around the Serial number which should grow monotonically.
259+ // Additionally, we use the MD5 to detect a conflict for
260+ // a given Serial.
261+ switch {
262+ case remoteState == nil && localState == nil :
263+ // Initialize a blank state
264+ out , _ := blankState (conf )
265+ if err := Persist (bytes .NewReader (out )); err != nil {
266+ return StateChangeNoop ,
267+ fmt .Errorf ("Failed to persist state: %v" , err )
268+ }
269+ return StateChangeNoop , nil
270+
271+ case remoteState == nil && localState != nil :
272+ fallthrough
273+ case remoteState .Serial < localState .Serial :
274+ // User should probably do a push, nothing to do
275+ return StateChangeLocalNewer , nil
276+
277+ case remoteState != nil && localState == nil :
278+ fallthrough
279+ case remoteState .Serial > localState .Serial :
280+ // Update the local state from the remote state
281+ if err := Persist (bytes .NewReader (payload .State )); err != nil {
282+ return StateChangeNoop ,
283+ fmt .Errorf ("Failed to persist state: %v" , err )
284+ }
285+ return StateChangeUpdateLocal , nil
286+
287+ case remoteState .Serial == localState .Serial :
288+ // Check for a hash collision on the local/remote state
289+ localMD5 := md5 .Sum (raw )
290+ if bytes .Equal (localMD5 [:md5 .Size ], payload .MD5 ) {
291+ // Hash collision, everything is up-to-date
292+ return StateChangeNoop , nil
293+ } else {
294+ // This is very bad. This means we have 2 state files
295+ // with the same Serial but a different hash. Most probably
296+ // explaination is two parallel apply operations. This
297+ // requires a manual reconciliation.
298+ return StateChangeConflict , nil
299+ }
300+ }
135301
136- // Return the blank state, which is done if the server
137- // returns a "not found" or equivalent
138- return blankState (conf )
302+ // We should not reach this point
303+ panic ("Unhandled remote update case" )
139304}
140305
141306// blankState is used to return a serialized form of a blank state
142307// with only the remote info.
143- func blankState (conf * terraform.RemoteState ) (io. Reader , error ) {
308+ func blankState (conf * terraform.RemoteState ) ([] byte , error ) {
144309 blank := terraform .NewState ()
145310 blank .Remote = conf
146311 buf := bytes .NewBuffer (nil )
147312 err := terraform .WriteState (blank , buf )
148- return buf , err
313+ return buf . Bytes () , err
149314}
150315
151316// Persist is used to write out the state given by a reader (likely
0 commit comments