Skip to content

Commit b3aaf6f

Browse files
committed
state/remote: add HTTP client
1 parent 4ec63bc commit b3aaf6f

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

‎state/remote/http.go‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package remote
2+
3+
import (
4+
"bytes"
5+
"crypto/md5"
6+
"encoding/base64"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
)
12+
13+
func httpFactory(conf map[string]string) (Client, error) {
14+
address, ok := conf["address"]
15+
if !ok {
16+
return nil, fmt.Errorf("missing 'address' configuration")
17+
}
18+
19+
url, err := url.Parse(address)
20+
if err != nil {
21+
return nil, fmt.Errorf("failed to parse HTTP URL: %s", err)
22+
}
23+
if url.Scheme != "http" && url.Scheme != "https" {
24+
return nil, fmt.Errorf("address must be HTTP or HTTPS")
25+
}
26+
27+
return &HTTPClient{
28+
URL: url,
29+
}, nil
30+
}
31+
32+
// HTTPClient is a remote client that stores data in Consul.
33+
type HTTPClient struct {
34+
URL *url.URL
35+
}
36+
37+
func (c *HTTPClient) Get() (*Payload, error) {
38+
resp, err := http.Get(c.URL.String())
39+
if err != nil {
40+
return nil, err
41+
}
42+
defer resp.Body.Close()
43+
44+
// Handle the common status codes
45+
switch resp.StatusCode {
46+
case http.StatusOK:
47+
// Handled after
48+
case http.StatusNoContent:
49+
return nil, nil
50+
case http.StatusNotFound:
51+
return nil, nil
52+
case http.StatusUnauthorized:
53+
return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
54+
case http.StatusForbidden:
55+
return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
56+
case http.StatusInternalServerError:
57+
return nil, fmt.Errorf("HTTP remote state internal server error")
58+
default:
59+
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
60+
}
61+
62+
// Read in the body
63+
buf := bytes.NewBuffer(nil)
64+
if _, err := io.Copy(buf, resp.Body); err != nil {
65+
return nil, fmt.Errorf("Failed to read remote state: %s", err)
66+
}
67+
68+
// Create the payload
69+
payload := &Payload{
70+
Data: buf.Bytes(),
71+
}
72+
73+
// Check for the MD5
74+
if raw := resp.Header.Get("Content-MD5"); raw != "" {
75+
md5, err := base64.StdEncoding.DecodeString(raw)
76+
if err != nil {
77+
return nil, fmt.Errorf(
78+
"Failed to decode Content-MD5 '%s': %s", raw, err)
79+
}
80+
81+
payload.MD5 = md5
82+
} else {
83+
// Generate the MD5
84+
hash := md5.Sum(payload.Data)
85+
payload.MD5 = hash[:]
86+
}
87+
88+
return payload, nil
89+
}
90+
91+
func (c *HTTPClient) Put(data []byte) error {
92+
// Copy the target URL
93+
base := *c.URL
94+
95+
// Generate the MD5
96+
hash := md5.Sum(data)
97+
b64 := base64.StdEncoding.EncodeToString(hash[:])
98+
99+
/*
100+
// Set the force query parameter if needed
101+
if force {
102+
values := base.Query()
103+
values.Set("force", "true")
104+
base.RawQuery = values.Encode()
105+
}
106+
*/
107+
108+
// Make the HTTP client and request
109+
req, err := http.NewRequest("POST", base.String(), bytes.NewReader(data))
110+
if err != nil {
111+
return fmt.Errorf("Failed to make HTTP request: %s", err)
112+
}
113+
114+
// Prepare the request
115+
req.Header.Set("Content-Type", "application/octet-stream")
116+
req.Header.Set("Content-MD5", b64)
117+
req.ContentLength = int64(len(data))
118+
119+
// Make the request
120+
resp, err := http.DefaultClient.Do(req)
121+
if err != nil {
122+
return fmt.Errorf("Failed to upload state: %v", err)
123+
}
124+
defer resp.Body.Close()
125+
126+
// Handle the error codes
127+
switch resp.StatusCode {
128+
case http.StatusOK:
129+
return nil
130+
default:
131+
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
132+
}
133+
}
134+
135+
func (c *HTTPClient) Delete() error {
136+
// Make the HTTP request
137+
req, err := http.NewRequest("DELETE", c.URL.String(), nil)
138+
if err != nil {
139+
return fmt.Errorf("Failed to make HTTP request: %s", err)
140+
}
141+
142+
// Make the request
143+
resp, err := http.DefaultClient.Do(req)
144+
if err != nil {
145+
return fmt.Errorf("Failed to delete state: %s", err)
146+
}
147+
defer resp.Body.Close()
148+
149+
// Handle the error codes
150+
switch resp.StatusCode {
151+
case http.StatusOK:
152+
return nil
153+
default:
154+
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
155+
}
156+
}

‎state/remote/http_test.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package remote
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestHTTPClient_impl(t *testing.T) {
8+
var _ Client = new(HTTPClient)
9+
}
10+
11+
func TestHTTPClient(t *testing.T) {
12+
// TODO
13+
//testClient(t, client)
14+
}

‎state/remote/remote.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ func NewClient(t string, conf map[string]string) (Client, error) {
3737
// NewClient.
3838
var BuiltinClients = map[string]Factory{
3939
"consul": consulFactory,
40+
"http": httpFactory,
4041
}

0 commit comments

Comments
 (0)