remote: delete package

This commit is contained in:
Mitchell Hashimoto 2015-02-23 10:52:13 -08:00
parent 84a0e512d3
commit abb523cc78
10 changed files with 0 additions and 2113 deletions

View File

@ -1,228 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
)
const (
// defaultAtlasServer is used when no address is given
defaultAtlasServer = "https://atlas.hashicorp.com/"
)
// AtlasRemoteClient implements the RemoteClient interface
// for an Atlas compatible server.
type AtlasRemoteClient struct {
server string
serverURL *url.URL
user string
name string
accessToken string
}
func NewAtlasRemoteClient(conf map[string]string) (*AtlasRemoteClient, error) {
client := &AtlasRemoteClient{}
if err := client.validateConfig(conf); err != nil {
return nil, err
}
return client, nil
}
func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
server, ok := conf["address"]
if !ok || server == "" {
server = defaultAtlasServer
}
url, err := url.Parse(server)
if err != nil {
return err
}
c.server = server
c.serverURL = url
token, ok := conf["access_token"]
if token == "" {
token = os.Getenv("ATLAS_TOKEN")
ok = true
}
if !ok || token == "" {
return fmt.Errorf(
"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
}
c.accessToken = token
name, ok := conf["name"]
if !ok || name == "" {
return fmt.Errorf("missing 'name' configuration")
}
parts := strings.Split(name, "/")
if len(parts) != 2 {
return fmt.Errorf("malformed name '%s'", name)
}
c.user = parts[0]
c.name = parts[1]
return nil
}
func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
// Make the HTTP request
req, err := http.NewRequest("GET", c.url().String(), nil)
if err != nil {
return nil, fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Request the url
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Handle the common status codes
switch resp.StatusCode {
case http.StatusOK:
// Handled after
case http.StatusNoContent:
return nil, nil
case http.StatusNotFound:
return nil, nil
case http.StatusUnauthorized:
return nil, ErrRequireAuth
case http.StatusForbidden:
return nil, ErrInvalidAuth
case http.StatusInternalServerError:
return nil, ErrRemoteInternal
default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
// Read in the body
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, resp.Body); err != nil {
return nil, fmt.Errorf("Failed to read remote state: %v", err)
}
// Create the payload
payload := &RemoteStatePayload{
State: buf.Bytes(),
}
// Check for the MD5
if raw := resp.Header.Get("Content-MD5"); raw != "" {
md5, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
}
payload.MD5 = md5
} else {
// Generate the MD5
hash := md5.Sum(payload.State)
payload.MD5 = hash[:md5.Size]
}
return payload, nil
}
func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
// Get the target URL
base := c.url()
// Generate the MD5
hash := md5.Sum(state)
b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
// Set the force query parameter if needed
if force {
values := base.Query()
values.Set("force", "true")
base.RawQuery = values.Encode()
}
// Make the HTTP client and request
req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Prepare the request
req.Header.Set("Content-MD5", b64)
req.Header.Set("Content-Type", "application/json")
req.ContentLength = int64(len(state))
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to upload state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusConflict:
return ErrConflict
case http.StatusPreconditionFailed:
return ErrServerNewer
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
default:
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}
func (c *AtlasRemoteClient) DeleteState() error {
// Make the HTTP request
req, err := http.NewRequest("DELETE", c.url().String(), nil)
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to delete state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusNoContent:
return nil
case http.StatusNotFound:
return nil
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
}
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
func (c *AtlasRemoteClient) url() *url.URL {
return &url.URL{
Scheme: c.serverURL.Scheme,
Host: c.serverURL.Host,
Path: path.Join("api/v1/terraform/state", c.user, c.name),
RawQuery: fmt.Sprintf("access_token=%s", c.accessToken),
}
}

View File

@ -1,128 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"os"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestAtlasRemote_Interface(t *testing.T) {
var client interface{} = &AtlasRemoteClient{}
if _, ok := client.(RemoteClient); !ok {
t.Fatalf("does not implement interface")
}
}
func checkAtlas(t *testing.T) {
if os.Getenv("ATLAS_TOKEN") == "" {
t.SkipNow()
}
}
func TestAtlasRemote_Validate(t *testing.T) {
conf := map[string]string{}
if _, err := NewAtlasRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["access_token"] = "test"
conf["name"] = "hashicorp/test-state"
if _, err := NewAtlasRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestAtlasRemote_Validate_envVar(t *testing.T) {
conf := map[string]string{}
if _, err := NewAtlasRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN"))
os.Setenv("ATLAS_TOKEN", "foo")
conf["name"] = "hashicorp/test-state"
if _, err := NewAtlasRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestAtlasRemote(t *testing.T) {
checkAtlas(t)
remote := &terraform.RemoteState{
Type: "atlas",
Config: map[string]string{
"access_token": os.Getenv("ATLAS_TOKEN"),
"name": "hashicorp/test-remote-state",
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
// Get a valid input
inp, err := blankState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
// Delete the state, should be none
err = r.DeleteState()
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure no state
payload, err := r.GetState()
if err != nil {
t.Fatalf("Err: %v", err)
}
if payload != nil {
t.Fatalf("unexpected payload")
}
// Put the state
err = r.PutState(inp, false)
if err != nil {
t.Fatalf("err: %v", err)
}
// Get it back
payload, err = r.GetState()
if err != nil {
t.Fatalf("Err: %v", err)
}
if payload == nil {
t.Fatalf("unexpected payload")
}
// Check the payload
if !bytes.Equal(payload.MD5, hash) {
t.Fatalf("bad hash: %x %x", payload.MD5, hash)
}
if !bytes.Equal(payload.State, inp) {
t.Errorf("inp: %s", inp)
t.Fatalf("bad response: %s", payload.State)
}
// Delete the state
err = r.DeleteState()
if err != nil {
t.Fatalf("err: %v", err)
}
// Should be gone
payload, err = r.GetState()
if err != nil {
t.Fatalf("Err: %v", err)
}
if payload != nil {
t.Fatalf("unexpected payload")
}
}

View File

@ -1,65 +0,0 @@
package remote
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/terraform"
)
var (
// ErrConflict is used to indicate the upload was rejected
// 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")
// ErrInvalidAuth is used if we provide authentication which
// is not valid
ErrInvalidAuth = fmt.Errorf("Invalid authentication")
// ErrRemoteInternal is used if we get an internal error
// from the remote server
ErrRemoteInternal = fmt.Errorf("Remote server reporting internal error")
)
type RemoteClient interface {
GetState() (*RemoteStatePayload, error)
PutState(state []byte, force bool) error
DeleteState() error
}
// RemoteStatePayload is used to return the remote state
// along with associated meta data when we do a remote fetch.
type RemoteStatePayload struct {
MD5 []byte
State []byte
}
// NewClientByState is used to construct a client from
// our remote state.
func NewClientByState(remote *terraform.RemoteState) (RemoteClient, error) {
return NewClientByType(remote.Type, remote.Config)
}
// NewClientByType is used to construct a RemoteClient
// based on the configured type.
func NewClientByType(ctype string, conf map[string]string) (RemoteClient, error) {
ctype = strings.ToLower(ctype)
switch ctype {
case "atlas":
return NewAtlasRemoteClient(conf)
case "consul":
return NewConsulRemoteClient(conf)
case "http":
return NewHTTPRemoteClient(conf)
default:
return nil, fmt.Errorf("Unknown remote client type '%s'", ctype)
}
}

View File

@ -1 +0,0 @@
package remote

View File

@ -1,77 +0,0 @@
package remote
import (
"crypto/md5"
"fmt"
consulapi "github.com/hashicorp/consul/api"
)
// ConsulRemoteClient implements the RemoteClient interface
// for an Consul compatible server.
type ConsulRemoteClient struct {
client *consulapi.Client
path string // KV path
}
func NewConsulRemoteClient(conf map[string]string) (*ConsulRemoteClient, error) {
client := &ConsulRemoteClient{}
if err := client.validateConfig(conf); err != nil {
return nil, err
}
return client, nil
}
func (c *ConsulRemoteClient) validateConfig(conf map[string]string) (err error) {
config := consulapi.DefaultConfig()
if token, ok := conf["access_token"]; ok && token != "" {
config.Token = token
}
if addr, ok := conf["address"]; ok && addr != "" {
config.Address = addr
}
path, ok := conf["path"]
if !ok || path == "" {
return fmt.Errorf("missing 'path' configuration")
}
c.path = path
c.client, err = consulapi.NewClient(config)
return err
}
func (c *ConsulRemoteClient) GetState() (*RemoteStatePayload, error) {
kv := c.client.KV()
pair, _, err := kv.Get(c.path, nil)
if err != nil {
return nil, err
}
if pair == nil {
return nil, nil
}
// Create the payload
payload := &RemoteStatePayload{
State: pair.Value,
}
// Generate the MD5
hash := md5.Sum(payload.State)
payload.MD5 = hash[:md5.Size]
return payload, nil
}
func (c *ConsulRemoteClient) PutState(state []byte, force bool) error {
pair := &consulapi.KVPair{
Key: c.path,
Value: state,
}
kv := c.client.KV()
_, err := kv.Put(pair, nil)
return err
}
func (c *ConsulRemoteClient) DeleteState() error {
kv := c.client.KV()
_, err := kv.Delete(c.path, nil)
return err
}

View File

@ -1,173 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"os"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/terraform"
)
func TestConsulRemote_Interface(t *testing.T) {
var client interface{} = &ConsulRemoteClient{}
if _, ok := client.(RemoteClient); !ok {
t.Fatalf("does not implement interface")
}
}
func checkConsul(t *testing.T) {
if os.Getenv("CONSUL_ADDR") == "" {
t.SkipNow()
}
}
func TestConsulRemote_Validate(t *testing.T) {
conf := map[string]string{}
if _, err := NewConsulRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["path"] = "test"
if _, err := NewConsulRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestConsulRemote_GetState(t *testing.T) {
checkConsul(t)
type tcase struct {
Path string
Body []byte
ExpectMD5 []byte
ExpectErr string
}
inp := []byte("testing")
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
cases := []*tcase{
&tcase{
Path: "foo",
Body: inp,
ExpectMD5: hash,
},
&tcase{
Path: "none",
},
}
for _, tc := range cases {
if tc.Body != nil {
conf := consulapi.DefaultConfig()
conf.Address = os.Getenv("CONSUL_ADDR")
client, _ := consulapi.NewClient(conf)
pair := &consulapi.KVPair{Key: tc.Path, Value: tc.Body}
client.KV().Put(pair, nil)
}
remote := &terraform.RemoteState{
Type: "consul",
Config: map[string]string{
"address": os.Getenv("CONSUL_ADDR"),
"path": tc.Path,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
payload, err := r.GetState()
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
if tc.ExpectMD5 != nil {
if payload == nil || !bytes.Equal(payload.MD5, tc.ExpectMD5) {
t.Fatalf("bad: %#v", payload)
}
}
if tc.Body != nil {
if !bytes.Equal(payload.State, tc.Body) {
t.Fatalf("bad: %#v", payload)
}
}
}
}
func TestConsulRemote_PutState(t *testing.T) {
checkConsul(t)
path := "foobar"
inp := []byte("testing")
remote := &terraform.RemoteState{
Type: "consul",
Config: map[string]string{
"address": os.Getenv("CONSUL_ADDR"),
"path": path,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.PutState(inp, false)
if err != nil {
t.Fatalf("err: %v", err)
}
conf := consulapi.DefaultConfig()
conf.Address = os.Getenv("CONSUL_ADDR")
client, _ := consulapi.NewClient(conf)
pair, _, err := client.KV().Get(path, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(pair.Value, inp) {
t.Fatalf("bad value")
}
}
func TestConsulRemote_DeleteState(t *testing.T) {
checkConsul(t)
path := "testdelete"
// Create the state
conf := consulapi.DefaultConfig()
conf.Address = os.Getenv("CONSUL_ADDR")
client, _ := consulapi.NewClient(conf)
pair := &consulapi.KVPair{Key: path, Value: []byte("test")}
client.KV().Put(pair, nil)
remote := &terraform.RemoteState{
Type: "consul",
Config: map[string]string{
"address": os.Getenv("CONSUL_ADDR"),
"path": path,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.DeleteState()
if err != nil {
t.Fatalf("Err: %v", err)
}
pair, _, err = client.KV().Get(path, nil)
if err != nil {
t.Fatalf("Err: %v", err)
}
if pair != nil {
t.Fatalf("state not deleted")
}
}

View File

@ -1,182 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
)
// HTTPRemoteClient implements the RemoteClient interface
// for an HTTP compatible server.
type HTTPRemoteClient struct {
// url is the URL that we GET / POST / DELETE to
url *url.URL
}
func NewHTTPRemoteClient(conf map[string]string) (*HTTPRemoteClient, error) {
client := &HTTPRemoteClient{}
if err := client.validateConfig(conf); err != nil {
return nil, err
}
return client, nil
}
func (c *HTTPRemoteClient) validateConfig(conf map[string]string) error {
urlRaw, ok := conf["address"]
if !ok || urlRaw == "" {
return fmt.Errorf("missing 'address' configuration")
}
url, err := url.Parse(urlRaw)
if err != nil {
return fmt.Errorf("failed to parse url: %v", err)
}
if url.Scheme != "http" && url.Scheme != "https" {
return fmt.Errorf("invalid url: %s", url)
}
c.url = url
return nil
}
func (c *HTTPRemoteClient) GetState() (*RemoteStatePayload, error) {
// Request the url
resp, err := http.Get(c.url.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Handle the common status codes
switch resp.StatusCode {
case http.StatusOK:
// Handled after
case http.StatusNoContent:
return nil, nil
case http.StatusNotFound:
return nil, nil
case http.StatusUnauthorized:
return nil, ErrRequireAuth
case http.StatusForbidden:
return nil, ErrInvalidAuth
case http.StatusInternalServerError:
return nil, ErrRemoteInternal
default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
// Read in the body
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, resp.Body); err != nil {
return nil, fmt.Errorf("Failed to read remote state: %v", err)
}
// Create the payload
payload := &RemoteStatePayload{
State: buf.Bytes(),
}
// Check for the MD5
if raw := resp.Header.Get("Content-MD5"); raw != "" {
md5, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
}
payload.MD5 = md5
} else {
// Generate the MD5
hash := md5.Sum(payload.State)
payload.MD5 = hash[:md5.Size]
}
return payload, nil
}
func (c *HTTPRemoteClient) PutState(state []byte, force bool) error {
// Copy the target URL
base := new(url.URL)
*base = *c.url
// Generate the MD5
hash := md5.Sum(state)
b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
// Set the force query parameter if needed
if force {
values := base.Query()
values.Set("force", "true")
base.RawQuery = values.Encode()
}
// Make the HTTP client and request
req, err := http.NewRequest("POST", base.String(), bytes.NewReader(state))
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Prepare the request
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-MD5", b64)
req.ContentLength = int64(len(state))
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to upload state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusConflict:
return ErrConflict
case http.StatusPreconditionFailed:
return ErrServerNewer
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
default:
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}
func (c *HTTPRemoteClient) DeleteState() error {
// Make the HTTP request
req, err := http.NewRequest("DELETE", c.url.String(), nil)
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to delete state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusNoContent:
return nil
case http.StatusNotFound:
return nil
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
default:
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}

View File

@ -1,331 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestHTTPRemote_Interface(t *testing.T) {
var client interface{} = &HTTPRemoteClient{}
if _, ok := client.(RemoteClient); !ok {
t.Fatalf("does not implement interface")
}
}
func TestHTTPRemote_Validate(t *testing.T) {
conf := map[string]string{}
if _, err := NewHTTPRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["address"] = ""
if _, err := NewHTTPRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["address"] = "*"
if _, err := NewHTTPRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["address"] = "http://cool.com"
if _, err := NewHTTPRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestHTTPRemote_GetState(t *testing.T) {
type tcase struct {
Code int
Header http.Header
Body []byte
ExpectMD5 []byte
ExpectErr string
}
inp := []byte("testing")
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
cases := []*tcase{
&tcase{
Code: http.StatusOK,
Body: inp,
ExpectMD5: hash,
},
&tcase{
Code: http.StatusNoContent,
},
&tcase{
Code: http.StatusNotFound,
},
&tcase{
Code: http.StatusInternalServerError,
ExpectErr: "Remote server reporting internal error",
},
&tcase{
Code: 418,
ExpectErr: "Unexpected HTTP response code 418",
},
}
for _, tc := range cases {
cb := func(resp http.ResponseWriter, req *http.Request) {
for k, v := range tc.Header {
resp.Header()[k] = v
}
resp.WriteHeader(tc.Code)
if tc.Body != nil {
resp.Write(tc.Body)
}
}
s := httptest.NewServer(http.HandlerFunc(cb))
defer s.Close()
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": s.URL,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
payload, err := r.GetState()
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
if tc.ExpectMD5 != nil {
if payload == nil || !bytes.Equal(payload.MD5, tc.ExpectMD5) {
t.Fatalf("bad: %#v", payload)
}
}
if tc.Body != nil {
if !bytes.Equal(payload.State, tc.Body) {
t.Fatalf("bad: %#v", payload)
}
}
}
}
func TestHTTPRemote_PutState(t *testing.T) {
type tcase struct {
Code int
Path string
Header http.Header
Body []byte
ExpectMD5 []byte
Force bool
ExpectErr string
}
inp := []byte("testing")
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
cases := []*tcase{
&tcase{
Code: http.StatusOK,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
},
&tcase{
Code: http.StatusOK,
Path: "/foobar?force=true",
Body: inp,
Force: true,
ExpectMD5: hash,
},
&tcase{
Code: http.StatusConflict,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrConflict.Error(),
},
&tcase{
Code: http.StatusPreconditionFailed,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrServerNewer.Error(),
},
&tcase{
Code: http.StatusUnauthorized,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrRequireAuth.Error(),
},
&tcase{
Code: http.StatusForbidden,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrInvalidAuth.Error(),
},
&tcase{
Code: http.StatusInternalServerError,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrRemoteInternal.Error(),
},
&tcase{
Code: 418,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: "Unexpected HTTP response code 418",
},
}
for _, tc := range cases {
cb := func(resp http.ResponseWriter, req *http.Request) {
for k, v := range tc.Header {
resp.Header()[k] = v
}
resp.WriteHeader(tc.Code)
// Verify the body
buf := bytes.NewBuffer(nil)
io.Copy(buf, req.Body)
if !bytes.Equal(buf.Bytes(), tc.Body) {
t.Fatalf("bad body: %v", buf.Bytes())
}
// Verify the path
req.URL.Host = ""
if req.URL.String() != tc.Path {
t.Fatalf("Bad path: %v %v", req.URL.String(), tc.Path)
}
// Verify the content length
if req.ContentLength != int64(len(tc.Body)) {
t.Fatalf("bad content length: %d", req.ContentLength)
}
// Verify the Content-MD5
b64 := req.Header.Get("Content-MD5")
raw, _ := base64.StdEncoding.DecodeString(b64)
if !bytes.Equal(raw, tc.ExpectMD5) {
t.Fatalf("bad md5: %v", raw)
}
}
s := httptest.NewServer(http.HandlerFunc(cb))
defer s.Close()
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": s.URL + "/foobar",
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.PutState(tc.Body, tc.Force)
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
}
}
func TestHTTPRemote_DeleteState(t *testing.T) {
type tcase struct {
Code int
Path string
Header http.Header
ExpectErr string
}
cases := []*tcase{
&tcase{
Code: http.StatusOK,
Path: "/foobar",
},
&tcase{
Code: http.StatusNoContent,
Path: "/foobar",
},
&tcase{
Code: http.StatusNotFound,
Path: "/foobar",
},
&tcase{
Code: http.StatusUnauthorized,
Path: "/foobar",
ExpectErr: ErrRequireAuth.Error(),
},
&tcase{
Code: http.StatusForbidden,
Path: "/foobar",
ExpectErr: ErrInvalidAuth.Error(),
},
&tcase{
Code: http.StatusInternalServerError,
Path: "/foobar",
ExpectErr: ErrRemoteInternal.Error(),
},
&tcase{
Code: 418,
Path: "/foobar",
ExpectErr: "Unexpected HTTP response code 418",
},
}
for _, tc := range cases {
cb := func(resp http.ResponseWriter, req *http.Request) {
for k, v := range tc.Header {
resp.Header()[k] = v
}
resp.WriteHeader(tc.Code)
// Verify the path
req.URL.Host = ""
if req.URL.String() != tc.Path {
t.Fatalf("Bad path: %v %v", req.URL.String(), tc.Path)
}
}
s := httptest.NewServer(http.HandlerFunc(cb))
defer s.Close()
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": s.URL + "/foobar",
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.DeleteState()
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
}
}

View File

@ -1,448 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/terraform/terraform"
)
const (
// LocalDirectory is the directory created in the working
// dir to hold the remote state file.
LocalDirectory = ".terraform"
// HiddenStateFile is the name of the state file in the
// LocalDirectory
HiddenStateFile = "terraform.tfstate"
// BackupHiddenStateFile is the path we backup the state
// file to before modifications are made
BackupHiddenStateFile = "terraform.tfstate.backup"
)
// StateChangeResult is used to communicate to a caller
// what actions have been taken when updating a state file
type StateChangeResult int
const (
// StateChangeNoop indicates nothing has happened,
// but that does not indicate an error. Everything is
// just up to date. (Push/Pull)
StateChangeNoop StateChangeResult = iota
// StateChangeInit indicates that there is no local or
// remote state, and that the state was initialized
StateChangeInit
// StateChangeUpdateLocal indicates the local state
// was updated. (Pull)
StateChangeUpdateLocal
// StateChangeUpdateRemote indicates the remote state
// was updated. (Push)
StateChangeUpdateRemote
// StateChangeLocalNewer means the pull was a no-op
// because the local state is newer than that of the
// server. This means a Push should take place. (Pull)
StateChangeLocalNewer
// StateChangeRemoteNewer means the push was a no-op
// because the remote state is newer than that of the
// local state. This means a Pull should take place.
// (Push)
StateChangeRemoteNewer
// StateChangeConflict means that the push or pull
// was a no-op because there is a conflict. This means
// there are multiple state definitions at the same
// serial number with different contents. This requires
// an operator to intervene and resolve the conflict.
// Shame on the user for doing concurrent apply.
// (Push/Pull)
StateChangeConflict
)
func (sc StateChangeResult) String() string {
switch sc {
case StateChangeNoop:
return "Local and remote state in sync"
case StateChangeInit:
return "Local state initialized"
case StateChangeUpdateLocal:
return "Local state updated"
case StateChangeUpdateRemote:
return "Remote state updated"
case StateChangeLocalNewer:
return "Local state is newer than remote state, push required"
case StateChangeRemoteNewer:
return "Remote state is newer than local state, pull required"
case StateChangeConflict:
return "Local and remote state conflict, manual resolution required"
default:
return fmt.Sprintf("Unknown state change type: %d", sc)
}
}
// SuccessfulPull is used to clasify the StateChangeResult for
// a pull operation. This is different by operation, but can be used
// to determine a proper exit code.
func (sc StateChangeResult) SuccessfulPull() bool {
switch sc {
case StateChangeNoop:
return true
case StateChangeInit:
return true
case StateChangeUpdateLocal:
return true
case StateChangeLocalNewer:
return false
case StateChangeConflict:
return false
default:
return false
}
}
// 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 {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Failed to get current directory: %v", err)
}
path := filepath.Join(cwd, LocalDirectory)
if err := os.Mkdir(path, 0770); err != nil {
if os.IsExist(err) {
return nil
}
return fmt.Errorf("Failed to make directory '%s': %v", path, err)
}
return nil
}
// HiddenStatePath is used to return the path to the hidden state file,
// should there be one.
// TODO: Rename to LocalStatePath
func HiddenStatePath() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("Failed to get current directory: %v", err)
}
path := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
return path, nil
}
// HaveLocalState is used to check if we have a local state file
func HaveLocalState() (bool, error) {
path, err := HiddenStatePath()
if err != nil {
return false, err
}
return ExistsFile(path)
}
// ExistsFile is used to check if a given file exists
func ExistsFile(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// ValidConfig does a purely logical validation of the remote config
func ValidConfig(conf *terraform.RemoteState) error {
// Default the type to Atlas
if conf.Type == "" {
conf.Type = "atlas"
}
_, err := NewClientByState(conf)
if err != nil {
return err
}
return nil
}
// ReadLocalState is used to read and parse the local state file
func ReadLocalState() (*terraform.State, []byte, error) {
path, err := HiddenStatePath()
if err != nil {
return nil, nil, err
}
// Open the existing file
raw, err := ioutil.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("Failed to open state file '%s': %s", path, err)
}
// Decode the state
state, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
return nil, nil, fmt.Errorf("Failed to read state file '%s': %v", path, err)
}
return state, raw, nil
}
// RefreshState is used to read the remote state given
// the configuration for the remote endpoint, and update
// the local state if necessary.
func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
if conf == nil {
return StateChangeNoop, fmt.Errorf("Missing remote server configuration")
}
// Read the state from the server
client, err := NewClientByState(conf)
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to create remote client: %v", err)
}
payload, err := client.GetState()
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to read remote state: %v", err)
}
// Parse the remote state
var remoteState *terraform.State
if payload != nil {
remoteState, err = terraform.ReadState(bytes.NewReader(payload.State))
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to parse remote state: %v", err)
}
// Ensure we understand the remote version!
if remoteState.Version > terraform.StateVersion {
return StateChangeNoop, fmt.Errorf(
`Remote state is version %d, this version of Terraform only understands up to %d`, remoteState.Version, terraform.StateVersion)
}
}
// Decode the state
localState, raw, err := ReadLocalState()
if err != nil {
return StateChangeNoop, err
}
// We need to handle the matrix of cases in reconciling
// the local and remote state. Primarily the concern is
// around the Serial number which should grow monotonically.
// Additionally, we use the MD5 to detect a conflict for
// a given Serial.
switch {
case remoteState == nil && localState == nil:
// Initialize a blank state
out, _ := blankState(conf)
if err := Persist(bytes.NewReader(out)); err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to persist state: %v", err)
}
return StateChangeInit, nil
case remoteState == nil && localState != nil:
// User should probably do a push, nothing to do
return StateChangeLocalNewer, nil
case remoteState != nil && localState == nil:
goto PERSIST
case remoteState.Serial < localState.Serial:
// User should probably do a push, nothing to do
return StateChangeLocalNewer, nil
case remoteState.Serial > localState.Serial:
goto PERSIST
case remoteState.Serial == localState.Serial:
// Check for a hash collision on the local/remote state
localMD5 := md5.Sum(raw)
if bytes.Equal(localMD5[:md5.Size], payload.MD5) {
// Hash collision, everything is up-to-date
return StateChangeNoop, nil
} else {
// This is very bad. This means we have 2 state files
// with the same Serial but a different hash. Most probably
// explaination is two parallel apply operations. This
// requires a manual reconciliation.
return StateChangeConflict, nil
}
default:
// We should not reach this point
panic("Unhandled remote update case")
}
PERSIST:
// Update the local state from the remote state
if err := Persist(bytes.NewReader(payload.State)); err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to persist state: %v", err)
}
return StateChangeUpdateLocal, nil
}
// 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) {
// Read the local state
_, raw, err := ReadLocalState()
if err != nil {
return StateChangeNoop, 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, err := NewClientByState(conf)
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to create remote client: %v", err)
}
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
}
}
// DeleteState is used to delete the remote state given
// the configuration for the remote endpoint.
func DeleteState(conf *terraform.RemoteState) error {
if conf == nil {
return fmt.Errorf("Missing remote server configuration")
}
// Setup the client
client, err := NewClientByState(conf)
if err != nil {
return fmt.Errorf("Failed to create remote client: %v", err)
}
// Destroy the state
err = client.DeleteState()
if err != nil {
return fmt.Errorf("Failed to delete remote state: %v", err)
}
return nil
}
// blankState is used to return a serialized form of a blank state
// with only the remote info.
func blankState(conf *terraform.RemoteState) ([]byte, error) {
blank := terraform.NewState()
blank.Remote = conf
buf := bytes.NewBuffer(nil)
err := terraform.WriteState(blank, buf)
return buf.Bytes(), err
}
// PersistState is used to persist out the given terraform state
// in our local state cache location.
func PersistState(s *terraform.State) error {
buf := bytes.NewBuffer(nil)
if err := terraform.WriteState(s, buf); err != nil {
return fmt.Errorf("Failed to encode state: %v", err)
}
if err := Persist(buf); err != nil {
return err
}
return nil
}
// Persist is used to write out the state given by a reader (likely
// being streamed from a remote server) to the local storage.
func Persist(r io.Reader) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Failed to get current directory: %v", err)
}
statePath := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
backupPath := filepath.Join(cwd, LocalDirectory, BackupHiddenStateFile)
// Backup the old file if it exists
if err := CopyFile(statePath, backupPath); err != nil {
return fmt.Errorf("Failed to backup state file '%s' to '%s': %v", statePath, backupPath, err)
}
// Open the state path
fh, err := os.Create(statePath)
if err != nil {
return fmt.Errorf("Failed to open state file '%s': %v", statePath, err)
}
// Copy the new state
_, err = io.Copy(fh, r)
fh.Close()
if err != nil {
os.Remove(statePath)
return fmt.Errorf("Failed to persist state file: %v", err)
}
return nil
}
// CopyFile is used to copy from a source file if it exists to a destination.
// This is used to create a backup of the state file.
func CopyFile(src, dst string) error {
srcFH, err := os.Open(src)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer srcFH.Close()
dstFH, err := os.Create(dst)
if err != nil {
return err
}
defer dstFH.Close()
_, err = io.Copy(dstFH, srcFH)
return err
}

View File

@ -1,480 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestEnsureDirectory(t *testing.T) {
err := EnsureDirectory()
if err != nil {
t.Fatalf("Err: %v", err)
}
cwd, _ := os.Getwd()
path := filepath.Join(cwd, LocalDirectory)
_, err = os.Stat(path)
if err != nil {
t.Fatalf("err: %v", err)
}
}
func TestHiddenStatePath(t *testing.T) {
path, err := HiddenStatePath()
if err != nil {
t.Fatalf("err: %v", err)
}
cwd, _ := os.Getwd()
expect := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
if path != expect {
t.Fatalf("bad: %v", path)
}
}
func TestValidConfig(t *testing.T) {
conf := &terraform.RemoteState{
Type: "",
Config: map[string]string{},
}
if err := ValidConfig(conf); err == nil {
t.Fatalf("blank should be not be valid: %v", err)
}
conf.Config["name"] = "hashicorp/test-remote-state"
conf.Config["access_token"] = "abcd"
if err := ValidConfig(conf); err != nil {
t.Fatalf("should be valid")
}
if conf.Type != "atlas" {
t.Fatalf("should default to atlas")
}
}
func TestRefreshState_Init(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemote(t, nil)
defer srv.Close()
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeInit {
t.Fatalf("bad: %s", sc)
}
local := testReadLocal(t)
if !local.Remote.Equals(remote) {
t.Fatalf("Bad: %#v", local)
}
if local.Serial != 1 {
t.Fatalf("Bad: %#v", local)
}
}
func TestRefreshState_NewVersion(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 100
rs.Version = terraform.StateVersion + 1
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 99
testWriteLocal(t, local)
_, err := RefreshState(remote)
if err == nil {
t.Fatalf("New version should fail!")
}
}
func TestRefreshState_Noop(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 100
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 100
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeNoop {
t.Fatalf("bad: %s", sc)
}
}
func TestRefreshState_UpdateLocal(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 100
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 99
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeUpdateLocal {
t.Fatalf("bad: %s", sc)
}
// Should update
local2 := testReadLocal(t)
if local2.Serial != 100 {
t.Fatalf("Bad: %#v", local2)
}
}
func TestRefreshState_LocalNewer(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 99
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 100
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeLocalNewer {
t.Fatalf("bad: %s", sc)
}
}
func TestRefreshState_Conflict(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 50
rs.RootModule().Outputs["foo"] = "bar"
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 50
local.RootModule().Outputs["foo"] = "baz"
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeConflict {
t.Fatalf("bad: %s", sc)
}
}
func TestPushState_NoState(t *testing.T) {
defer testFixCwd(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 testFixCwd(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 testFixCwd(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 testFixCwd(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 testFixCwd(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 TestDeleteState(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 200)
defer srv.Close()
local := terraform.NewState()
testWriteLocal(t, local)
err := DeleteState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
}
func TestBlankState(t *testing.T) {
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": "http://foo.com/",
},
}
r, err := blankState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
s, err := terraform.ReadState(bytes.NewReader(r))
if err != nil {
t.Fatalf("err: %v", err)
}
if !remote.Equals(s.Remote) {
t.Fatalf("remote mismatch")
}
}
func TestPersist(t *testing.T) {
tmp, cwd := testDir(t)
defer testFixCwd(tmp, cwd)
EnsureDirectory()
// Place old state file, should backup
old := filepath.Join(tmp, LocalDirectory, HiddenStateFile)
ioutil.WriteFile(old, []byte("test"), 0777)
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": "http://foo.com/",
},
}
blank, _ := blankState(remote)
if err := Persist(bytes.NewReader(blank)); err != nil {
t.Fatalf("err: %v", err)
}
// Check for backup
backup := filepath.Join(tmp, LocalDirectory, BackupHiddenStateFile)
out, err := ioutil.ReadFile(backup)
if err != nil {
t.Fatalf("Err: %v", err)
}
if string(out) != "test" {
t.Fatalf("bad: %v", out)
}
// Read the state
out, err = ioutil.ReadFile(old)
if err != nil {
t.Fatalf("Err: %v", err)
}
s, err := terraform.ReadState(bytes.NewReader(out))
if err != nil {
t.Fatalf("Err: %v", err)
}
// Check the remote
if !remote.Equals(s.Remote) {
t.Fatalf("remote mismatch")
}
}
// testRemote is used to make a test HTTP server to
// return a given state file
func testRemote(t *testing.T, s *terraform.State) (*terraform.RemoteState, *httptest.Server) {
var b64md5 string
buf := bytes.NewBuffer(nil)
if s != nil {
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
md5 := md5.Sum(buf.Bytes())
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
}
cb := func(resp http.ResponseWriter, req *http.Request) {
if s == nil {
resp.WriteHeader(404)
return
}
resp.Header().Set("Content-MD5", b64md5)
resp.Write(buf.Bytes())
}
srv := httptest.NewServer(http.HandlerFunc(cb))
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": srv.URL,
},
}
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{
Type: "http",
Config: map[string]string{
"address": 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) {
tmp, err := ioutil.TempDir("", "remote")
if err != nil {
t.Fatalf("err: %v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %v", err)
}
os.Chdir(tmp)
if err := EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err)
}
return tmp, cwd
}
// testFixCwd is used to as a defer to testDir
func testFixCwd(tmp, cwd string) {
os.Chdir(cwd)
os.RemoveAll(tmp)
}
// testReadLocal is used to just get the local state
func testReadLocal(t *testing.T) *terraform.State {
path, err := HiddenStatePath()
if err != nil {
t.Fatalf("err: %v", err)
}
raw, err := ioutil.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("err: %v", err)
}
if raw == nil {
return nil
}
s, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
t.Fatalf("err: %v", err)
}
return s
}
// testWriteLocal is used to write the local state
func testWriteLocal(t *testing.T, s *terraform.State) {
path, err := HiddenStatePath()
if err != nil {
t.Fatalf("err: %v", err)
}
buf := bytes.NewBuffer(nil)
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
err = ioutil.WriteFile(path, buf.Bytes(), 0777)
if err != nil {
t.Fatalf("err: %v", err)
}
}