TUN-3410: Request the v1 Tunnelstore API

This commit is contained in:
Adam Chalmers 2020-09-22 16:28:05 -05:00 committed by Areg Harutyunyan
parent e2ff7f65fc
commit 61f3fab757
2 changed files with 196 additions and 35 deletions

View File

@ -27,6 +27,7 @@ var (
ErrUnauthorized = errors.New("unauthorized") ErrUnauthorized = errors.New("unauthorized")
ErrBadRequest = errors.New("incorrect request parameters") ErrBadRequest = errors.New("incorrect request parameters")
ErrNotFound = errors.New("not found") ErrNotFound = errors.New("not found")
ErrAPINoSuccess = errors.New("API call failed")
) )
type Tunnel struct { type Tunnel struct {
@ -39,7 +40,7 @@ type Tunnel struct {
type Connection struct { type Connection struct {
ColoName string `json:"colo_name"` ColoName string `json:"colo_name"`
ID uuid.UUID `json:"uuid"` ID uuid.UUID `json:"id"`
IsPendingReconnect bool `json:"is_pending_reconnect"` IsPendingReconnect bool `json:"is_pending_reconnect"`
} }
@ -91,11 +92,9 @@ func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) { func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result DNSRouteResult var result DNSRouteResult
if err := json.NewDecoder(body).Decode(&result); err != nil { err := parseResponse(body, &result)
return nil, err
}
result.route = dr result.route = dr
return &result, nil return &result, err
} }
func (dr *DNSRoute) RecordType() string { func (dr *DNSRoute) RecordType() string {
@ -152,11 +151,9 @@ func (lr *LBRoute) RecordType() string {
func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) { func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result LBRouteResult var result LBRouteResult
if err := json.NewDecoder(body).Decode(&result); err != nil { err := parseResponse(body, &result)
return nil, err
}
result.route = lr result.route = lr
return &result, nil return &result, err
} }
func (res *LBRouteResult) SuccessSummary() string { func (res *LBRouteResult) SuccessSummary() string {
@ -313,16 +310,18 @@ func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
var tunnels []*Tunnel return parseListTunnels(resp.Body)
if err := json.NewDecoder(resp.Body).Decode(&tunnels); err != nil {
return nil, errors.Wrap(err, "failed to decode response")
}
return tunnels, nil
} }
return nil, r.statusCodeToError("list tunnels", resp) return nil, r.statusCodeToError("list tunnels", resp)
} }
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
}
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error { func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
endpoint := r.baseEndpoints.accountLevel endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID)) endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
@ -370,15 +369,48 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
req.Header.Set("Content-Type", jsonContentType) req.Header.Set("Content-Type", jsonContentType)
} }
req.Header.Add("X-Auth-User-Service-Key", r.authToken) req.Header.Add("X-Auth-User-Service-Key", r.authToken)
req.Header.Add("Accept", "application/json;version=1")
return r.client.Do(req) return r.client.Do(req)
} }
func parseResponse(reader io.Reader, data interface{}) error {
// Schema for Tunnelstore responses in the v1 API.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result struct {
Result json.RawMessage `json:"result"`
Success bool `json:"success"`
Errors []string `json:"errors"`
}
// First, parse the wrapper and check the API call succeeded
if err := json.NewDecoder(reader).Decode(&result); err != nil {
return errors.Wrap(err, "failed to decode response")
}
if err := checkErrors(result.Errors); err != nil {
return err
}
if !result.Success {
return ErrAPINoSuccess
}
// At this point we know the API call succeeded, so, parse out the inner
// result into the datatype provided as a parameter.
return json.Unmarshal(result.Result, &data)
}
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) { func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
var tunnel Tunnel var tunnel Tunnel
if err := json.NewDecoder(reader).Decode(&tunnel); err != nil { err := parseResponse(reader, &tunnel)
return nil, errors.Wrap(err, "failed to decode response") return &tunnel, err
} }
return &tunnel, nil
func checkErrors(errs []string) error {
if len(errs) == 0 {
return nil
}
if len(errs) == 1 {
return fmt.Errorf("API error: %s", errs[0])
}
allErrs := strings.Join(errs, "; ")
return fmt.Errorf("API errors: %s", allErrs)
} }
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error { func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {

View File

@ -1,9 +1,16 @@
package tunnelstore package tunnelstore
import ( import (
"bytes"
"fmt"
"io"
"io/ioutil"
"reflect"
"strings" "strings"
"testing" "testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -12,7 +19,7 @@ func TestDNSRouteUnmarshalResult(t *testing.T) {
userHostname: "example.com", userHostname: "example.com",
} }
result, err := route.UnmarshalResult(strings.NewReader(`{"cname": "new"}`)) result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"cname": "new"}}`))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, &DNSRouteResult{ assert.Equal(t, &DNSRouteResult{
@ -20,9 +27,20 @@ func TestDNSRouteUnmarshalResult(t *testing.T) {
CName: ChangeNew, CName: ChangeNew,
}, result) }, result)
_, err = route.UnmarshalResult(strings.NewReader(`abc`)) badJSON := []string{
`abc`,
`{"success": false, "result": {"cname": "new"}}`,
`{"errors": ["foo"], "result": {"cname": "new"}}`,
`{"errors": ["foo", "bar"], "result": {"cname": "new"}}`,
`{"result": {"cname": "new"}}`,
`{"result": {"cname": "new"}}`,
}
for _, j := range badJSON {
_, err = route.UnmarshalResult(strings.NewReader(j))
assert.NotNil(t, err) assert.NotNil(t, err)
} }
}
func TestLBRouteUnmarshalResult(t *testing.T) { func TestLBRouteUnmarshalResult(t *testing.T) {
route := &LBRoute{ route := &LBRoute{
@ -30,7 +48,7 @@ func TestLBRouteUnmarshalResult(t *testing.T) {
lbPool: "pool", lbPool: "pool",
} }
result, err := route.UnmarshalResult(strings.NewReader(`{"pool": "unchanged", "load_balancer": "updated"}`)) result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"pool": "unchanged", "load_balancer": "updated"}}`))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, &LBRouteResult{ assert.Equal(t, &LBRouteResult{
@ -39,9 +57,19 @@ func TestLBRouteUnmarshalResult(t *testing.T) {
Pool: ChangeUnchanged, Pool: ChangeUnchanged,
}, result) }, result)
_, err = route.UnmarshalResult(strings.NewReader(`abc`)) badJSON := []string{
`abc`,
`{"success": false, "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"errors": ["foo"], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"errors": ["foo", "bar"], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
`{"result": {"pool": "unchanged", "load_balancer": "updated"}}`,
}
for _, j := range badJSON {
_, err = route.UnmarshalResult(strings.NewReader(j))
assert.NotNil(t, err) assert.NotNil(t, err)
} }
}
func TestLBRouteResultSuccessSummary(t *testing.T) { func TestLBRouteResultSuccessSummary(t *testing.T) {
route := &LBRoute{ route := &LBRoute{
@ -76,3 +104,104 @@ func TestLBRouteResultSuccessSummary(t *testing.T) {
assert.Equal(t, tt.expected, actual, "case %d", i+1) assert.Equal(t, tt.expected, actual, "case %d", i+1)
} }
} }
func Test_parseListTunnels(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want []*Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": []}`},
want: []*Tunnel{},
},
{
name: "success is false",
args: args{body: `{"success": false, "result": []}`},
wantErr: true,
},
{
name: "errors are present",
args: args{body: `{"errors": ["foo"], "result": []}`},
wantErr: true,
},
{
name: "invalid response",
args: args{body: `abc`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
got, err := parseListTunnels(body)
if (err != nil) != tt.wantErr {
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unmarshalTunnel(t *testing.T) {
type args struct {
reader io.Reader
}
tests := []struct {
name string
args args
want *Tunnel
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := unmarshalTunnel(tt.args.reader)
if (err != nil) != tt.wantErr {
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnmarshalTunnelOk(t *testing.T) {
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{
ID: uuid.Nil,
Name: "test",
CreatedAt: time.Time{},
Connections: []Connection{},
}
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, &expected, actual)
}
func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{
`abc`,
`{"success": true, "result": abc}`,
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
`{"errors": ["foo"], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
}
for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
}
}