diff --git a/remote/client.go b/remote/client.go new file mode 100644 index 000000000..8aee8aef7 --- /dev/null +++ b/remote/client.go @@ -0,0 +1,114 @@ +package remote + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + + "github.com/hashicorp/terraform/terraform" +) + +// RemoteStatePayload is used to return the remote state +// along with associated meta data when we do a remote fetch. +type RemoteStatePayload struct { + MD5 []byte + R io.Reader +} + +// GetState is used to read the remote state +func GetState(conf *terraform.RemoteState) (*RemoteStatePayload, error) { + // Get the base URL configuration + base, err := url.Parse(conf.Server) + if err != nil { + return nil, fmt.Errorf("Failed to parse remote server '%s': %v", conf.Server, err) + } + + // Compute the full path by just appending the name + base.Path = path.Join(base.Path, conf.Name) + + // Add the request token if any + if conf.AuthToken != "" { + values := base.Query() + values.Set("access_token", conf.AuthToken) + base.RawQuery = values.Encode() + } + + // Request the url + resp, err := http.Get(base.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, fmt.Errorf("Remote server requires authentication") + case http.StatusForbidden: + return nil, fmt.Errorf("Invalid authentication token") + case http.StatusInternalServerError: + return nil, fmt.Errorf("Remote server reporting internal error") + default: + return nil, fmt.Errorf("Received 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{ + R: buf, + } + + // Check if this is Consul + if raw := resp.Header.Get("X-Consul-Index"); raw != "" { + // Check if we used the ?raw query param, otherwise decode + if _, ok := base.Query()["raw"]; !ok { + type kv struct { + Value []byte + } + var values []*kv + if err := json.Unmarshal(buf.Bytes(), &values); err != nil { + return nil, fmt.Errorf("Failed to decode Consul response: %v", err) + } + + // Setup the reader to pull the value from Consul + payload.R = bytes.NewReader(values[0].Value) + + // Generate the MD5 + hash := md5.Sum(values[0].Value) + payload.MD5 = hash[:md5.Size] + } + } + + // 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 if _, ok := payload.R.(*bytes.Buffer); ok { + // Generate the MD5 + hash := md5.Sum(buf.Bytes()) + payload.MD5 = hash[:md5.Size] + } + + return payload, nil +} diff --git a/remote/client_test.go b/remote/client_test.go new file mode 100644 index 000000000..73a5e6e48 --- /dev/null +++ b/remote/client_test.go @@ -0,0 +1,76 @@ +package remote + +import ( + "bytes" + "crypto/md5" + "io" + "net/http" + "strings" + "testing" + + "github.com/armon/consul-api" + "github.com/hashicorp/terraform/terraform" +) + +var haveInternet bool + +func init() { + // Use google to check if we are on the net + _, err := http.Get("http://www.google.com") + haveInternet = (err == nil) +} + +func TestGetState_Consul(t *testing.T) { + if !haveInternet { + t.SkipNow() + } + + // Use the Consul demo cluster + conf := consulapi.DefaultConfig() + conf.Address = "demo.consul.io:80" + client, err := consulapi.NewClient(conf) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Write some test data + pair := &consulapi.KVPair{ + Key: "test/tf/remote/foobar", + Value: []byte("testing"), + } + kv := client.KV() + if _, err := kv.Put(pair, nil); err != nil { + t.Fatalf("err: %v", err) + } + defer kv.Delete(pair.Key, nil) + + // Check we can get the state + remote := &terraform.RemoteState{ + Name: "foobar", + Server: "http://demo.consul.io/v1/kv/test/tf/remote", + } +REQ: + payload, err := GetState(remote) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check the MD5 + expect := md5.Sum(pair.Value) + if !bytes.Equal(payload.MD5, expect[:md5.Size]) { + t.Fatalf("Bad md5") + } + + // Check the body + var buf bytes.Buffer + io.Copy(&buf, payload.R) + if string(buf.Bytes()) != "testing" { + t.Fatalf("Bad body") + } + + // Try doing a ?raw lookup + if !strings.Contains(remote.Server, "?raw") { + remote.Server += "?raw" + goto REQ + } +}