Skip to content

Commit d332b8a

Browse files
armonmitchellh
authored andcommitted
remote: implement refresh state
1 parent d077a82 commit d332b8a

3 files changed

Lines changed: 198 additions & 21 deletions

File tree

‎command/init.go‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,18 @@ func (c *InitCommand) Run(args []string) int {
9999

100100
// Handle remote state if configured
101101
if !remoteConf.Empty() {
102-
// Read the updated state file
103-
remoteR, err := remote.ReadState(&remoteConf)
102+
change, err := remote.RefreshState(&remoteConf)
104103
if err != nil {
105104
c.Ui.Error(fmt.Sprintf(
106-
"Failed to read remote state: %v", err))
105+
"Failed to refresh from remote state: %v", err))
107106
return 1
108107
}
109108

110-
// Persist the remote state
111-
if err := remote.Persist(remoteR); err != nil {
112-
c.Ui.Error(fmt.Sprintf(
113-
"Failed to persist state: %v", err))
109+
// Log the change that took place
110+
c.Ui.Output(fmt.Sprintf("%s", change))
111+
112+
// Use an error exit code if the update was not a success
113+
if !change.SuccessfulPull() {
114114
return 1
115115
}
116116
}

‎remote/remote.go‎

Lines changed: 176 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package remote
22

33
import (
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
34112
func 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

‎remote/remote_test.go‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,19 @@ func TestValidateConfig(t *testing.T) {
6868
// TODO:
6969
}
7070

71-
func TestReadState(t *testing.T) {
71+
func TestRefreshState_Blank(t *testing.T) {
72+
// TODO
73+
}
74+
75+
func TestRefreshState_Update_Newer(t *testing.T) {
76+
// TODO
77+
}
78+
79+
func TestRefreshState_Update_Older(t *testing.T) {
80+
// TODO
81+
}
82+
83+
func TestRefreshState_Noop(t *testing.T) {
7284
// TODO
7385
}
7486

@@ -82,7 +94,7 @@ func TestBlankState(t *testing.T) {
8294
if err != nil {
8395
t.Fatalf("err: %v", err)
8496
}
85-
s, err := terraform.ReadState(r)
97+
s, err := terraform.ReadState(bytes.NewReader(r))
8698
if err != nil {
8799
t.Fatalf("err: %v", err)
88100
}
@@ -116,7 +128,7 @@ func TestPersist(t *testing.T) {
116128
AuthToken: "foobar",
117129
}
118130
blank, _ := blankState(remote)
119-
if err := Persist(blank); err != nil {
131+
if err := Persist(bytes.NewReader(blank)); err != nil {
120132
t.Fatalf("err: %v", err)
121133
}
122134

0 commit comments

Comments
 (0)