remote: delete package
This commit is contained in:
parent
84a0e512d3
commit
abb523cc78
228
remote/atlas.go
228
remote/atlas.go
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package remote
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
182
remote/http.go
182
remote/http.go
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
448
remote/remote.go
448
remote/remote.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue