Pass at much more flexible remote/http backend

- Configurable Put (store) method, default POST to preserve behavior
- Configurable Lock method & address
- Configurable Unlock method & address

More thorough testing still needed, but this if functional
This commit is contained in:
Ross McFarland 2017-08-19 10:31:47 -07:00
parent 06b4b509d7
commit 69546c4b33
2 changed files with 137 additions and 112 deletions

View File

@ -22,13 +22,53 @@ func httpFactory(conf map[string]string) (Client, error) {
return nil, fmt.Errorf("missing 'address' configuration") return nil, fmt.Errorf("missing 'address' configuration")
} }
url, err := url.Parse(address) storeURL, err := url.Parse(address)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse HTTP URL: %s", err) return nil, fmt.Errorf("failed to parse address URL: %s", err)
} }
if url.Scheme != "http" && url.Scheme != "https" { if storeURL.Scheme != "http" && storeURL.Scheme != "https" {
return nil, fmt.Errorf("address must be HTTP or HTTPS") return nil, fmt.Errorf("address must be HTTP or HTTPS")
} }
storeMethod, ok := conf["store_method"]
if !ok {
storeMethod = "POST"
}
var lockURL *url.URL
lockAddress, ok := conf["lock_address"]
if ok {
lockURL, err := url.Parse(lockAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse lockAddress URL: %s", err)
}
if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
return nil, fmt.Errorf("lockAddress must be HTTP or HTTPS")
}
} else {
lockURL = nil
}
lockMethod, ok := conf["lock_method"]
if !ok {
lockMethod = "LOCK"
}
var unlockURL *url.URL
unlockAddress, ok := conf["unlock_address"]
if ok {
unlockURL, err := url.Parse(unlockAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse unlockAddress URL: %s", err)
}
if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
return nil, fmt.Errorf("unlockAddress must be HTTP or HTTPS")
}
} else {
unlockURL = nil
}
unlockMethod, ok := conf["unlock_method"]
if !ok {
unlockMethod = "UNLOCK"
}
client := &http.Client{} client := &http.Client{}
if skipRaw, ok := conf["skip_cert_verification"]; ok { if skipRaw, ok := conf["skip_cert_verification"]; ok {
@ -48,58 +88,72 @@ func httpFactory(conf map[string]string) (Client, error) {
} }
} }
supportsLocking := false ret := &HTTPClient{
if supportsLockingRaw, ok := conf["supports_locking"]; ok { URL: storeURL,
var err error StoreMethod: storeMethod,
supportsLocking, err = strconv.ParseBool(supportsLockingRaw)
if err != nil { LockURL: lockURL,
return nil, fmt.Errorf("supports_locking must be boolean") LockMethod: lockMethod,
} UnlockURL: unlockURL,
UnlockMethod: unlockMethod,
Client: client,
Username: conf["username"],
Password: conf["password"],
} }
ret := &HTTPClient{
URL: url,
Client: client,
SupportsLocking: supportsLocking,
}
if username, ok := conf["username"]; ok && username != "" {
ret.Username = username
}
if password, ok := conf["password"]; ok && password != "" {
ret.Password = password
}
return ret, nil return ret, nil
} }
// HTTPClient is a remote client that stores data in Consul or HTTP REST. // HTTPClient is a remote client that stores data in Consul or HTTP REST.
type HTTPClient struct { type HTTPClient struct {
URL *url.URL // Store & Retrieve
Client *http.Client URL *url.URL
Username string StoreMethod string
Password string
SupportsLocking bool // Locking
lockID string LockURL *url.URL
LockMethod string
UnlockURL *url.URL
UnlockMethod string
// HTTP
Client *http.Client
Username string
Password string
lockID string
jsonLockInfo []byte
} }
func (c *HTTPClient) httpPost(url string, data []byte, what string) (*http.Response, error) { func (c *HTTPClient) httpRequest(method string, url *url.URL, data *[]byte, what string) (*http.Response, error) {
// If we have data we need a reader
// Generate the MD5 var reader io.Reader = nil
hash := md5.Sum(data) if data != nil {
b64 := base64.StdEncoding.EncodeToString(hash[:]) reader = bytes.NewReader(*data)
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("Failed to make HTTP request: %s", err)
} }
// Prepare the request // Create the request
req.Header.Set("Content-Type", "application/json") req, err := http.NewRequest(method, url.String(), reader)
req.Header.Set("Content-MD5", b64) if err != nil {
req.ContentLength = int64(len(data)) return nil, fmt.Errorf("Failed to make %s HTTP request: %s", what, err)
}
// Setup basic auth
if c.Username != "" { if c.Username != "" {
req.SetBasicAuth(c.Username, c.Password) req.SetBasicAuth(c.Username, c.Password)
} }
// Work with data/body
if data != nil {
req.Header.Set("Content-Type", "application/json")
req.ContentLength = int64(len(*data))
// Generate the MD5
hash := md5.Sum(*data)
b64 := base64.StdEncoding.EncodeToString(hash[:])
req.Header.Set("Content-MD5", b64)
}
// Make the request // Make the request
resp, err := c.Client.Do(req) resp, err := c.Client.Do(req)
if err != nil { if err != nil {
@ -110,20 +164,13 @@ func (c *HTTPClient) httpPost(url string, data []byte, what string) (*http.Respo
} }
func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) { func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) {
if !c.SupportsLocking { if c.LockURL == nil {
return "", nil return "", nil
} }
c.lockID = "" c.lockID = ""
url := *c.URL jsonLockInfo := info.Marshal()
path := url.Path resp, err := c.httpRequest(c.LockMethod, c.LockURL, &jsonLockInfo, "lock")
if len(path) == 0 || path[len(path)-1] != byte('/') {
// add a trailing /
path = fmt.Sprintf("%s/", path)
}
url.Path = fmt.Sprintf("%slock", path)
resp, err := c.httpPost(url.String(), info.Marshal(), "lock")
if err != nil { if err != nil {
return "", err return "", err
} }
@ -132,6 +179,7 @@ func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) {
switch resp.StatusCode { switch resp.StatusCode {
case http.StatusOK: case http.StatusOK:
c.lockID = info.ID c.lockID = info.ID
c.jsonLockInfo = jsonLockInfo
return info.ID, nil return info.ID, nil
case http.StatusUnauthorized: case http.StatusUnauthorized:
return "", fmt.Errorf("HTTP remote state endpoint requires auth") return "", fmt.Errorf("HTTP remote state endpoint requires auth")
@ -155,26 +203,11 @@ func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) {
} }
func (c *HTTPClient) Unlock(id string) error { func (c *HTTPClient) Unlock(id string) error {
if !c.SupportsLocking { if c.UnlockURL == nil {
return nil return nil
} }
// copy the target URL resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
url := *c.URL
path := url.Path
if len(path) == 0 || path[len(path)-1] != byte('/') {
// add a trailing /
path = fmt.Sprintf("%s/", path)
}
url.Path = fmt.Sprintf("%sunlock", path)
if c.SupportsLocking {
query := url.Query()
query.Set("ID", id)
url.RawQuery = query.Encode()
}
resp, err := c.httpPost(url.String(), []byte{}, "unlock")
if err != nil { if err != nil {
return err return err
} }
@ -189,18 +222,7 @@ func (c *HTTPClient) Unlock(id string) error {
} }
func (c *HTTPClient) Get() (*Payload, error) { func (c *HTTPClient) Get() (*Payload, error) {
req, err := http.NewRequest("GET", c.URL.String(), nil) resp, err := c.httpRequest("GET", c.URL, nil, "get state")
if err != nil {
return nil, err
}
// Prepare the request
if c.Username != "" {
req.SetBasicAuth(c.Username, c.Password)
}
// Make the request
resp, err := c.Client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -262,7 +284,7 @@ func (c *HTTPClient) Put(data []byte) error {
// Copy the target URL // Copy the target URL
base := *c.URL base := *c.URL
if c.SupportsLocking { if c.lockID != "" {
query := base.Query() query := base.Query()
query.Set("ID", c.lockID) query.Set("ID", c.lockID)
base.RawQuery = query.Encode() base.RawQuery = query.Encode()
@ -277,7 +299,11 @@ func (c *HTTPClient) Put(data []byte) error {
} }
*/ */
resp, err := c.httpPost(base.String(), data, "upload state") var method string = "POST"
if c.StoreMethod != "" {
method = c.StoreMethod
}
resp, err := c.httpRequest(method, &base, &data, "upload state")
if err != nil { if err != nil {
return err return err
} }
@ -293,20 +319,10 @@ func (c *HTTPClient) Put(data []byte) error {
} }
func (c *HTTPClient) Delete() error { func (c *HTTPClient) Delete() error {
req, err := http.NewRequest("DELETE", c.URL.String(), nil)
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %s", err)
}
// Prepare the request
if c.Username != "" {
req.SetBasicAuth(c.Username, c.Password)
}
// Make the request // Make the request
resp, err := c.Client.Do(req) resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state")
if err != nil { if err != nil {
return fmt.Errorf("Failed to delete state: %s", err) return err
} }
defer resp.Body.Close() defer resp.Body.Close()

View File

@ -30,8 +30,22 @@ func TestHTTPClient(t *testing.T) {
client := &HTTPClient{URL: url, Client: cleanhttp.DefaultClient()} client := &HTTPClient{URL: url, Client: cleanhttp.DefaultClient()}
testClient(t, client) testClient(t, client)
a := &HTTPClient{URL: url, Client: cleanhttp.DefaultClient(), SupportsLocking: true} a := &HTTPClient{
b := &HTTPClient{URL: url, Client: cleanhttp.DefaultClient(), SupportsLocking: true} URL: url,
LockURL: url,
LockMethod: "LOCK",
UnlockURL: url,
UnlockMethod: "UNLOCK",
Client: cleanhttp.DefaultClient(),
}
b := &HTTPClient{
URL: url,
LockURL: url,
LockMethod: "LOCK",
UnlockURL: url,
UnlockMethod: "UNLOCK",
Client: cleanhttp.DefaultClient(),
}
TestRemoteLocks(t, a, b) TestRemoteLocks(t, a, b)
} }
@ -45,25 +59,20 @@ func (h *testHTTPHandler) Handle(w http.ResponseWriter, r *http.Request) {
case "GET": case "GET":
w.Write(h.Data) w.Write(h.Data)
case "POST": case "POST":
switch r.URL.Path { buf := new(bytes.Buffer)
case "/": if _, err := io.Copy(buf, r.Body); err != nil {
// state w.WriteHeader(500)
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r.Body); err != nil {
w.WriteHeader(500)
}
h.Data = buf.Bytes()
case "/lock":
if h.Locked {
w.WriteHeader(409)
} else {
h.Locked = true
}
case "/unlock":
h.Locked = false
} }
h.Data = buf.Bytes()
case "LOCK":
if h.Locked {
w.WriteHeader(409)
} else {
h.Locked = true
}
case "UNLOCK":
h.Locked = false
case "DELETE": case "DELETE":
h.Data = nil h.Data = nil
w.WriteHeader(200) w.WriteHeader(200)