remote: Working on API client
This commit is contained in:
parent
e36b46de07
commit
cc4e48e0ec
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue