Skip to content

Commit 6822e4f

Browse files
committed
TUN-5482: Refactor tunnelstore client related packages for more coherent package
1 parent 834c0ea commit 6822e4f

27 files changed

+1056
-1025
lines changed

cfapi/base_client.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package cfapi
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"strings"
11+
"time"
12+
13+
"github.com/pkg/errors"
14+
"github.com/rs/zerolog"
15+
"golang.org/x/net/http2"
16+
)
17+
18+
const (
19+
defaultTimeout = 15 * time.Second
20+
jsonContentType = "application/json"
21+
)
22+
23+
var (
24+
ErrUnauthorized = errors.New("unauthorized")
25+
ErrBadRequest = errors.New("incorrect request parameters")
26+
ErrNotFound = errors.New("not found")
27+
ErrAPINoSuccess = errors.New("API call failed")
28+
)
29+
30+
type RESTClient struct {
31+
baseEndpoints *baseEndpoints
32+
authToken string
33+
userAgent string
34+
client http.Client
35+
log *zerolog.Logger
36+
}
37+
38+
type baseEndpoints struct {
39+
accountLevel url.URL
40+
zoneLevel url.URL
41+
accountRoutes url.URL
42+
accountVnets url.URL
43+
}
44+
45+
var _ Client = (*RESTClient)(nil)
46+
47+
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
48+
if strings.HasSuffix(baseURL, "/") {
49+
baseURL = baseURL[:len(baseURL)-1]
50+
}
51+
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
52+
if err != nil {
53+
return nil, errors.Wrap(err, "failed to create account level endpoint")
54+
}
55+
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
56+
if err != nil {
57+
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
58+
}
59+
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
60+
if err != nil {
61+
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
62+
}
63+
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
64+
if err != nil {
65+
return nil, errors.Wrap(err, "failed to create account level endpoint")
66+
}
67+
httpTransport := http.Transport{
68+
TLSHandshakeTimeout: defaultTimeout,
69+
ResponseHeaderTimeout: defaultTimeout,
70+
}
71+
http2.ConfigureTransport(&httpTransport)
72+
return &RESTClient{
73+
baseEndpoints: &baseEndpoints{
74+
accountLevel: *accountLevelEndpoint,
75+
zoneLevel: *zoneLevelEndpoint,
76+
accountRoutes: *accountRoutesEndpoint,
77+
accountVnets: *accountVnetsEndpoint,
78+
},
79+
authToken: authToken,
80+
userAgent: userAgent,
81+
client: http.Client{
82+
Transport: &httpTransport,
83+
Timeout: defaultTimeout,
84+
},
85+
log: log,
86+
}, nil
87+
}
88+
89+
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
90+
var bodyReader io.Reader
91+
if body != nil {
92+
if bodyBytes, err := json.Marshal(body); err != nil {
93+
return nil, errors.Wrap(err, "failed to serialize json body")
94+
} else {
95+
bodyReader = bytes.NewBuffer(bodyBytes)
96+
}
97+
}
98+
99+
req, err := http.NewRequest(method, url.String(), bodyReader)
100+
if err != nil {
101+
return nil, errors.Wrapf(err, "can't create %s request", method)
102+
}
103+
req.Header.Set("User-Agent", r.userAgent)
104+
if bodyReader != nil {
105+
req.Header.Set("Content-Type", jsonContentType)
106+
}
107+
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
108+
req.Header.Add("Accept", "application/json;version=1")
109+
return r.client.Do(req)
110+
}
111+
112+
func parseResponse(reader io.Reader, data interface{}) error {
113+
// Schema for Tunnelstore responses in the v1 API.
114+
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
115+
var result response
116+
// First, parse the wrapper and check the API call succeeded
117+
if err := json.NewDecoder(reader).Decode(&result); err != nil {
118+
return errors.Wrap(err, "failed to decode response")
119+
}
120+
if err := result.checkErrors(); err != nil {
121+
return err
122+
}
123+
if !result.Success {
124+
return ErrAPINoSuccess
125+
}
126+
// At this point we know the API call succeeded, so, parse out the inner
127+
// result into the datatype provided as a parameter.
128+
if err := json.Unmarshal(result.Result, &data); err != nil {
129+
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
130+
}
131+
return nil
132+
}
133+
134+
type response struct {
135+
Success bool `json:"success,omitempty"`
136+
Errors []apiErr `json:"errors,omitempty"`
137+
Messages []string `json:"messages,omitempty"`
138+
Result json.RawMessage `json:"result,omitempty"`
139+
}
140+
141+
func (r *response) checkErrors() error {
142+
if len(r.Errors) == 0 {
143+
return nil
144+
}
145+
if len(r.Errors) == 1 {
146+
return r.Errors[0]
147+
}
148+
var messages string
149+
for _, e := range r.Errors {
150+
messages += fmt.Sprintf("%s; ", e)
151+
}
152+
return fmt.Errorf("API errors: %s", messages)
153+
}
154+
155+
type apiErr struct {
156+
Code json.Number `json:"code,omitempty"`
157+
Message string `json:"message,omitempty"`
158+
}
159+
160+
func (e apiErr) Error() string {
161+
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
162+
}
163+
164+
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
165+
if resp.Header.Get("Content-Type") == "application/json" {
166+
var errorsResp response
167+
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
168+
if err := errorsResp.checkErrors(); err != nil {
169+
return errors.Errorf("Failed to %s: %s", op, err)
170+
}
171+
}
172+
}
173+
174+
switch resp.StatusCode {
175+
case http.StatusOK:
176+
return nil
177+
case http.StatusBadRequest:
178+
return ErrBadRequest
179+
case http.StatusUnauthorized, http.StatusForbidden:
180+
return ErrUnauthorized
181+
case http.StatusNotFound:
182+
return ErrNotFound
183+
}
184+
return errors.Errorf("API call to %s failed with status %d: %s", op,
185+
resp.StatusCode, http.StatusText(resp.StatusCode))
186+
}

cfapi/client.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package cfapi
2+
3+
import (
4+
"github.com/google/uuid"
5+
)
6+
7+
type TunnelClient interface {
8+
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
9+
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
10+
DeleteTunnel(tunnelID uuid.UUID) error
11+
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
12+
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
13+
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
14+
}
15+
16+
type HostnameClient interface {
17+
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
18+
}
19+
20+
type IPRouteClient interface {
21+
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
22+
AddRoute(newRoute NewRoute) (Route, error)
23+
DeleteRoute(params DeleteRouteParams) error
24+
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
25+
}
26+
27+
type VnetClient interface {
28+
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
29+
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
30+
DeleteVirtualNetwork(id uuid.UUID) error
31+
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
32+
}
33+
34+
type Client interface {
35+
TunnelClient
36+
HostnameClient
37+
IPRouteClient
38+
VnetClient
39+
}

0 commit comments

Comments
 (0)