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
|
// due to a conflict on the state
|
||||||
ErrConflict = fmt.Errorf("Conflicting state file")
|
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
|
// ErrRequireAuth is used if the remote server requires
|
||||||
// authentication and none is provided
|
// authentication and none is provided
|
||||||
ErrRequireAuth = fmt.Errorf("Remote server requires authentication")
|
ErrRequireAuth = fmt.Errorf("Remote server requires authentication")
|
||||||
|
@ -187,6 +191,8 @@ func (r *remoteStateClient) PutState(state []byte, force bool) error {
|
||||||
return nil
|
return nil
|
||||||
case http.StatusConflict:
|
case http.StatusConflict:
|
||||||
return ErrConflict
|
return ErrConflict
|
||||||
|
case http.StatusPreconditionFailed:
|
||||||
|
return ErrServerNewer
|
||||||
case http.StatusUnauthorized:
|
case http.StatusUnauthorized:
|
||||||
return ErrRequireAuth
|
return ErrRequireAuth
|
||||||
case http.StatusForbidden:
|
case http.StatusForbidden:
|
||||||
|
|
|
@ -193,6 +193,13 @@ func TestPutState(t *testing.T) {
|
||||||
ExpectMD5: hash,
|
ExpectMD5: hash,
|
||||||
ExpectErr: ErrConflict.Error(),
|
ExpectErr: ErrConflict.Error(),
|
||||||
},
|
},
|
||||||
|
&tcase{
|
||||||
|
Code: http.StatusPreconditionFailed,
|
||||||
|
Path: "/foobar",
|
||||||
|
Body: inp,
|
||||||
|
ExpectMD5: hash,
|
||||||
|
ExpectErr: ErrServerNewer.Error(),
|
||||||
|
},
|
||||||
&tcase{
|
&tcase{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
Path: "/foobar",
|
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
|
// EnsureDirectory is used to make sure the local storage
|
||||||
// directory exists
|
// directory exists
|
||||||
func EnsureDirectory() error {
|
func EnsureDirectory() error {
|
||||||
|
@ -312,6 +330,45 @@ func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
|
||||||
panic("Unhandled remote update case")
|
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
|
// blankState is used to return a serialized form of a blank state
|
||||||
// with only the remote info.
|
// with only the remote info.
|
||||||
func blankState(conf *terraform.RemoteState) ([]byte, error) {
|
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) {
|
func TestBlankState(t *testing.T) {
|
||||||
remote := &terraform.RemoteState{
|
remote := &terraform.RemoteState{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
|
@ -337,6 +424,20 @@ func testRemote(t *testing.T, s *terraform.State) (*terraform.RemoteState, *http
|
||||||
return remote, srv
|
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
|
// testDir is used to change the current working directory
|
||||||
// into a test directory that should be remoted after
|
// into a test directory that should be remoted after
|
||||||
func testDir(t *testing.T) (string, string) {
|
func testDir(t *testing.T) (string, string) {
|
||||||
|
|
Loading…
Reference in New Issue