remote: Supporting push state
This commit is contained in:
parent
d1e41bc992
commit
bec1dfcd68
|
@ -19,6 +19,10 @@ var (
|
|||
// due to a conflict on the state
|
||||
ErrConflict = fmt.Errorf("Conflicting state file")
|
||||
|
||||
// ErrServerNewer is used to indicate the serial number of
|
||||
// the state is newer on the server side
|
||||
ErrServerNewer = fmt.Errorf("Server-side Serial is newer")
|
||||
|
||||
// ErrRequireAuth is used if the remote server requires
|
||||
// authentication and none is provided
|
||||
ErrRequireAuth = fmt.Errorf("Remote server requires authentication")
|
||||
|
@ -187,6 +191,8 @@ func (r *remoteStateClient) PutState(state []byte, force bool) error {
|
|||
return nil
|
||||
case http.StatusConflict:
|
||||
return ErrConflict
|
||||
case http.StatusPreconditionFailed:
|
||||
return ErrServerNewer
|
||||
case http.StatusUnauthorized:
|
||||
return ErrRequireAuth
|
||||
case http.StatusForbidden:
|
||||
|
|
|
@ -193,6 +193,13 @@ func TestPutState(t *testing.T) {
|
|||
ExpectMD5: hash,
|
||||
ExpectErr: ErrConflict.Error(),
|
||||
},
|
||||
&tcase{
|
||||
Code: http.StatusPreconditionFailed,
|
||||
Path: "/foobar",
|
||||
Body: inp,
|
||||
ExpectMD5: hash,
|
||||
ExpectErr: ErrServerNewer.Error(),
|
||||
},
|
||||
&tcase{
|
||||
Code: http.StatusUnauthorized,
|
||||
Path: "/foobar",
|
||||
|
|
|
@ -115,6 +115,24 @@ func (sc StateChangeResult) SuccessfulPull() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// SuccessfulPush is used to clasify the StateChangeResult for
|
||||
// a push operation. This is different by operation, but can be used
|
||||
// to determine a proper exit code
|
||||
func (sc StateChangeResult) SuccessfulPush() bool {
|
||||
switch sc {
|
||||
case StateChangeNoop:
|
||||
return true
|
||||
case StateChangeUpdateRemote:
|
||||
return true
|
||||
case StateChangeRemoteNewer:
|
||||
return false
|
||||
case StateChangeConflict:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureDirectory is used to make sure the local storage
|
||||
// directory exists
|
||||
func EnsureDirectory() error {
|
||||
|
@ -312,6 +330,45 @@ func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
|
|||
panic("Unhandled remote update case")
|
||||
}
|
||||
|
||||
// PushState is used to read the local state and
|
||||
// update the remote state if necessary. The state push
|
||||
// can be 'forced' to override any conflict detection
|
||||
// on the server-side.
|
||||
func PushState(conf *terraform.RemoteState, force bool) (StateChangeResult, error) {
|
||||
// Get the path to the state file
|
||||
path, err := HiddenStatePath()
|
||||
if err != nil {
|
||||
return StateChangeNoop, err
|
||||
}
|
||||
|
||||
// Get the existing state file
|
||||
raw, err := ioutil.ReadFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return StateChangeNoop, fmt.Errorf("Failed to read local state: %v", err)
|
||||
}
|
||||
|
||||
// Check if there is no local state
|
||||
if raw == nil {
|
||||
return StateChangeNoop, fmt.Errorf("No local state to push")
|
||||
}
|
||||
|
||||
// Push the state to the server
|
||||
client := &remoteStateClient{conf: conf}
|
||||
err = client.PutState(raw, force)
|
||||
|
||||
// Handle the various edge cases
|
||||
switch err {
|
||||
case nil:
|
||||
return StateChangeUpdateRemote, nil
|
||||
case ErrServerNewer:
|
||||
return StateChangeRemoteNewer, nil
|
||||
case ErrConflict:
|
||||
return StateChangeConflict, nil
|
||||
default:
|
||||
return StateChangeNoop, err
|
||||
}
|
||||
}
|
||||
|
||||
// blankState is used to return a serialized form of a blank state
|
||||
// with only the remote info.
|
||||
func blankState(conf *terraform.RemoteState) ([]byte, error) {
|
||||
|
|
|
@ -241,6 +241,93 @@ func TestRefreshState_Conflict(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPushState_NoState(t *testing.T) {
|
||||
defer fixDir(testDir(t))
|
||||
|
||||
remote, srv := testRemotePush(t, 200)
|
||||
defer srv.Close()
|
||||
|
||||
sc, err := PushState(remote, false)
|
||||
if err.Error() != "No local state to push" {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sc != StateChangeNoop {
|
||||
t.Fatalf("Bad: %v", sc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushState_Update(t *testing.T) {
|
||||
defer fixDir(testDir(t))
|
||||
|
||||
remote, srv := testRemotePush(t, 200)
|
||||
defer srv.Close()
|
||||
|
||||
local := terraform.NewState()
|
||||
testWriteLocal(t, local)
|
||||
|
||||
sc, err := PushState(remote, false)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sc != StateChangeUpdateRemote {
|
||||
t.Fatalf("Bad: %v", sc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushState_RemoteNewer(t *testing.T) {
|
||||
defer fixDir(testDir(t))
|
||||
|
||||
remote, srv := testRemotePush(t, 412)
|
||||
defer srv.Close()
|
||||
|
||||
local := terraform.NewState()
|
||||
testWriteLocal(t, local)
|
||||
|
||||
sc, err := PushState(remote, false)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sc != StateChangeRemoteNewer {
|
||||
t.Fatalf("Bad: %v", sc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushState_Conflict(t *testing.T) {
|
||||
defer fixDir(testDir(t))
|
||||
|
||||
remote, srv := testRemotePush(t, 409)
|
||||
defer srv.Close()
|
||||
|
||||
local := terraform.NewState()
|
||||
testWriteLocal(t, local)
|
||||
|
||||
sc, err := PushState(remote, false)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sc != StateChangeConflict {
|
||||
t.Fatalf("Bad: %v", sc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushState_Error(t *testing.T) {
|
||||
defer fixDir(testDir(t))
|
||||
|
||||
remote, srv := testRemotePush(t, 500)
|
||||
defer srv.Close()
|
||||
|
||||
local := terraform.NewState()
|
||||
testWriteLocal(t, local)
|
||||
|
||||
sc, err := PushState(remote, false)
|
||||
if err != ErrRemoteInternal {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sc != StateChangeNoop {
|
||||
t.Fatalf("Bad: %v", sc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlankState(t *testing.T) {
|
||||
remote := &terraform.RemoteState{
|
||||
Name: "foo",
|
||||
|
@ -337,6 +424,20 @@ func testRemote(t *testing.T, s *terraform.State) (*terraform.RemoteState, *http
|
|||
return remote, srv
|
||||
}
|
||||
|
||||
// testRemotePush is used to make a test HTTP server to
|
||||
// return a given status code on push
|
||||
func testRemotePush(t *testing.T, c int) (*terraform.RemoteState, *httptest.Server) {
|
||||
cb := func(resp http.ResponseWriter, req *http.Request) {
|
||||
resp.WriteHeader(c)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(cb))
|
||||
remote := &terraform.RemoteState{
|
||||
Name: "foo",
|
||||
Server: srv.URL,
|
||||
}
|
||||
return remote, srv
|
||||
}
|
||||
|
||||
// testDir is used to change the current working directory
|
||||
// into a test directory that should be remoted after
|
||||
func testDir(t *testing.T) (string, string) {
|
||||
|
|
Loading…
Reference in New Issue