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:
parent
06b4b509d7
commit
69546c4b33
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue