TUN-5482: Refactor tunnelstore client related packages for more coherent package
This commit is contained in:
parent
834c0eaeed
commit
6822e4f8ab
|
@ -0,0 +1,186 @@
|
||||||
|
package cfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeout = 15 * time.Second
|
||||||
|
jsonContentType = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
ErrBadRequest = errors.New("incorrect request parameters")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrAPINoSuccess = errors.New("API call failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type RESTClient struct {
|
||||||
|
baseEndpoints *baseEndpoints
|
||||||
|
authToken string
|
||||||
|
userAgent string
|
||||||
|
client http.Client
|
||||||
|
log *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseEndpoints struct {
|
||||||
|
accountLevel url.URL
|
||||||
|
zoneLevel url.URL
|
||||||
|
accountRoutes url.URL
|
||||||
|
accountVnets url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Client = (*RESTClient)(nil)
|
||||||
|
|
||||||
|
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||||
|
if strings.HasSuffix(baseURL, "/") {
|
||||||
|
baseURL = baseURL[:len(baseURL)-1]
|
||||||
|
}
|
||||||
|
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||||
|
}
|
||||||
|
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||||
|
}
|
||||||
|
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
||||||
|
}
|
||||||
|
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||||
|
}
|
||||||
|
httpTransport := http.Transport{
|
||||||
|
TLSHandshakeTimeout: defaultTimeout,
|
||||||
|
ResponseHeaderTimeout: defaultTimeout,
|
||||||
|
}
|
||||||
|
http2.ConfigureTransport(&httpTransport)
|
||||||
|
return &RESTClient{
|
||||||
|
baseEndpoints: &baseEndpoints{
|
||||||
|
accountLevel: *accountLevelEndpoint,
|
||||||
|
zoneLevel: *zoneLevelEndpoint,
|
||||||
|
accountRoutes: *accountRoutesEndpoint,
|
||||||
|
accountVnets: *accountVnetsEndpoint,
|
||||||
|
},
|
||||||
|
authToken: authToken,
|
||||||
|
userAgent: userAgent,
|
||||||
|
client: http.Client{
|
||||||
|
Transport: &httpTransport,
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
},
|
||||||
|
log: log,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||||
|
} else {
|
||||||
|
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url.String(), bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", r.userAgent)
|
||||||
|
if bodyReader != nil {
|
||||||
|
req.Header.Set("Content-Type", jsonContentType)
|
||||||
|
}
|
||||||
|
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||||
|
req.Header.Add("Accept", "application/json;version=1")
|
||||||
|
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 response
|
||||||
|
// 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 := result.checkErrors(); 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.
|
||||||
|
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||||
|
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Success bool `json:"success,omitempty"`
|
||||||
|
Errors []apiErr `json:"errors,omitempty"`
|
||||||
|
Messages []string `json:"messages,omitempty"`
|
||||||
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *response) checkErrors() error {
|
||||||
|
if len(r.Errors) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(r.Errors) == 1 {
|
||||||
|
return r.Errors[0]
|
||||||
|
}
|
||||||
|
var messages string
|
||||||
|
for _, e := range r.Errors {
|
||||||
|
messages += fmt.Sprintf("%s; ", e)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("API errors: %s", messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiErr struct {
|
||||||
|
Code json.Number `json:"code,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e apiErr) Error() string {
|
||||||
|
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||||
|
if resp.Header.Get("Content-Type") == "application/json" {
|
||||||
|
var errorsResp response
|
||||||
|
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
||||||
|
if err := errorsResp.checkErrors(); err != nil {
|
||||||
|
return errors.Errorf("Failed to %s: %s", op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
return nil
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
return ErrBadRequest
|
||||||
|
case http.StatusUnauthorized, http.StatusForbidden:
|
||||||
|
return ErrUnauthorized
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||||
|
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package cfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TunnelClient interface {
|
||||||
|
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||||
|
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||||
|
DeleteTunnel(tunnelID uuid.UUID) error
|
||||||
|
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||||
|
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||||
|
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostnameClient interface {
|
||||||
|
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPRouteClient interface {
|
||||||
|
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||||
|
AddRoute(newRoute NewRoute) (Route, error)
|
||||||
|
DeleteRoute(params DeleteRouteParams) error
|
||||||
|
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VnetClient interface {
|
||||||
|
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
|
||||||
|
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
|
||||||
|
DeleteVirtualNetwork(id uuid.UUID) error
|
||||||
|
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
TunnelClient
|
||||||
|
HostnameClient
|
||||||
|
IPRouteClient
|
||||||
|
VnetClient
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package cfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Change = string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChangeNew = "new"
|
||||||
|
ChangeUpdated = "updated"
|
||||||
|
ChangeUnchanged = "unchanged"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostnameRoute represents a record type that can route to a tunnel
|
||||||
|
type HostnameRoute interface {
|
||||||
|
json.Marshaler
|
||||||
|
RecordType() string
|
||||||
|
UnmarshalResult(body io.Reader) (HostnameRouteResult, error)
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostnameRouteResult interface {
|
||||||
|
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||||
|
SuccessSummary() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSRoute struct {
|
||||||
|
userHostname string
|
||||||
|
overwriteExisting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSRouteResult struct {
|
||||||
|
route *DNSRoute
|
||||||
|
CName Change `json:"cname"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute {
|
||||||
|
return &DNSRoute{
|
||||||
|
userHostname: userHostname,
|
||||||
|
overwriteExisting: overwriteExisting,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||||
|
s := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
UserHostname string `json:"user_hostname"`
|
||||||
|
OverwriteExisting bool `json:"overwrite_existing"`
|
||||||
|
}{
|
||||||
|
Type: dr.RecordType(),
|
||||||
|
UserHostname: dr.userHostname,
|
||||||
|
OverwriteExisting: dr.overwriteExisting,
|
||||||
|
}
|
||||||
|
return json.Marshal(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||||
|
var result DNSRouteResult
|
||||||
|
err := parseResponse(body, &result)
|
||||||
|
result.route = dr
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DNSRoute) RecordType() string {
|
||||||
|
return "dns"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *DNSRoute) String() string {
|
||||||
|
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *DNSRouteResult) SuccessSummary() string {
|
||||||
|
var msgFmt string
|
||||||
|
switch res.CName {
|
||||||
|
case ChangeNew:
|
||||||
|
msgFmt = "Added CNAME %s which will route to this tunnel"
|
||||||
|
case ChangeUpdated: // this is not currently returned by tunnelsore
|
||||||
|
msgFmt = "%s updated to route to your tunnel"
|
||||||
|
case ChangeUnchanged:
|
||||||
|
msgFmt = "%s is already configured to route to your tunnel"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(msgFmt, res.hostname())
|
||||||
|
}
|
||||||
|
|
||||||
|
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||||
|
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||||
|
func (res *DNSRouteResult) hostname() string {
|
||||||
|
if res.Name != "" {
|
||||||
|
return res.Name
|
||||||
|
}
|
||||||
|
return res.route.userHostname
|
||||||
|
}
|
||||||
|
|
||||||
|
type LBRoute struct {
|
||||||
|
lbName string
|
||||||
|
lbPool string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LBRouteResult struct {
|
||||||
|
route *LBRoute
|
||||||
|
LoadBalancer Change `json:"load_balancer"`
|
||||||
|
Pool Change `json:"pool"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLBRoute(lbName, lbPool string) HostnameRoute {
|
||||||
|
return &LBRoute{
|
||||||
|
lbName: lbName,
|
||||||
|
lbPool: lbPool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||||
|
s := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
LBName string `json:"lb_name"`
|
||||||
|
LBPool string `json:"lb_pool"`
|
||||||
|
}{
|
||||||
|
Type: lr.RecordType(),
|
||||||
|
LBName: lr.lbName,
|
||||||
|
LBPool: lr.lbPool,
|
||||||
|
}
|
||||||
|
return json.Marshal(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lr *LBRoute) RecordType() string {
|
||||||
|
return "lb"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lb *LBRoute) String() string {
|
||||||
|
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||||
|
var result LBRouteResult
|
||||||
|
err := parseResponse(body, &result)
|
||||||
|
result.route = lr
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *LBRouteResult) SuccessSummary() string {
|
||||||
|
var msg string
|
||||||
|
switch res.LoadBalancer + "," + res.Pool {
|
||||||
|
case "new,new":
|
||||||
|
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
||||||
|
case "new,updated":
|
||||||
|
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
||||||
|
case "new,unchanged":
|
||||||
|
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
||||||
|
case "updated,new":
|
||||||
|
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
||||||
|
case "updated,updated":
|
||||||
|
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
||||||
|
case "updated,unchanged":
|
||||||
|
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
||||||
|
case "unchanged,updated":
|
||||||
|
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
||||||
|
case "unchanged,unchanged":
|
||||||
|
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
||||||
|
case "unchanged,new":
|
||||||
|
// this state is not possible
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) {
|
||||||
|
endpoint := r.baseEndpoints.zoneLevel
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
||||||
|
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return route.UnmarshalResult(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("add route", resp)
|
||||||
|
}
|
|
@ -1,17 +1,9 @@
|
||||||
package tunnelstore
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,125 +97,3 @@ 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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnmarshalConnections(t *testing.T) {
|
|
||||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
|
||||||
expected := ActiveClient{
|
|
||||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
|
||||||
Features: []string{"ha-origin"},
|
|
||||||
Version: "2021.2.5",
|
|
||||||
Arch: "darwin_amd64",
|
|
||||||
RunAt: time.Time{},
|
|
||||||
Connections: []Connection{{
|
|
||||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
|
||||||
ColoName: "LIS",
|
|
||||||
IsPendingReconnect: false,
|
|
||||||
OriginIP: net.ParseIP("148.38.28.2"),
|
|
||||||
OpenedAt: time.Time{},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
|
||||||
}
|
|
|
@ -1,9 +1,13 @@
|
||||||
package teamnet
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -133,3 +137,104 @@ type GetRouteByIpParams struct {
|
||||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||||
VNetID *uuid.UUID
|
VNetID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||||
|
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
|
endpoint.RawQuery = filter.Encode()
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseListDetailedRoutes(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("list routes", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||||
|
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||||
|
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||||
|
if err != nil {
|
||||||
|
return Route{}, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseRoute(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Route{}, r.statusCodeToError("add route", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||||
|
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
||||||
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||||
|
setVnetParam(&endpoint, params.VNetID)
|
||||||
|
|
||||||
|
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
_, err := parseRoute(resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.statusCodeToError("delete route", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIP checks which route will proxy a given IP.
|
||||||
|
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||||
|
setVnetParam(&endpoint, params.VNetID)
|
||||||
|
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseDetailedRoute(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
||||||
|
var routes []*DetailedRoute
|
||||||
|
err := parseResponse(body, &routes)
|
||||||
|
return routes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||||
|
var route Route
|
||||||
|
err := parseResponse(body, &route)
|
||||||
|
return route, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
|
||||||
|
var route DetailedRoute
|
||||||
|
err := parseResponse(body, &route)
|
||||||
|
return route, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
|
||||||
|
// virtual network (if one is provided).
|
||||||
|
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||||
|
queryParams := url.Values{}
|
||||||
|
if vnetID != nil {
|
||||||
|
queryParams.Set("virtual_network_id", vnetID.String())
|
||||||
|
}
|
||||||
|
endpoint.RawQuery = queryParams.Encode()
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package teamnet
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -13,90 +13,90 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
filterDeleted = cli.BoolFlag{
|
filterIpRouteDeleted = cli.BoolFlag{
|
||||||
Name: "filter-is-deleted",
|
Name: "filter-is-deleted",
|
||||||
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||||
}
|
}
|
||||||
filterTunnelID = cli.StringFlag{
|
filterIpRouteTunnelID = cli.StringFlag{
|
||||||
Name: "filter-tunnel-id",
|
Name: "filter-tunnel-id",
|
||||||
Usage: "Show only routes with the given tunnel ID.",
|
Usage: "Show only routes with the given tunnel ID.",
|
||||||
}
|
}
|
||||||
filterSubset = cli.StringFlag{
|
filterSubsetIpRoute = cli.StringFlag{
|
||||||
Name: "filter-network-is-subset-of",
|
Name: "filter-network-is-subset-of",
|
||||||
Aliases: []string{"nsub"},
|
Aliases: []string{"nsub"},
|
||||||
Usage: "Show only routes whose network is a subset of the given network.",
|
Usage: "Show only routes whose network is a subset of the given network.",
|
||||||
}
|
}
|
||||||
filterSuperset = cli.StringFlag{
|
filterSupersetIpRoute = cli.StringFlag{
|
||||||
Name: "filter-network-is-superset-of",
|
Name: "filter-network-is-superset-of",
|
||||||
Aliases: []string{"nsup"},
|
Aliases: []string{"nsup"},
|
||||||
Usage: "Show only routes whose network is a superset of the given network.",
|
Usage: "Show only routes whose network is a superset of the given network.",
|
||||||
}
|
}
|
||||||
filterComment = cli.StringFlag{
|
filterIpRouteComment = cli.StringFlag{
|
||||||
Name: "filter-comment-is",
|
Name: "filter-comment-is",
|
||||||
Usage: "Show only routes with this comment.",
|
Usage: "Show only routes with this comment.",
|
||||||
}
|
}
|
||||||
filterVnet = cli.StringFlag{
|
filterIpRouteByVnet = cli.StringFlag{
|
||||||
Name: "filter-virtual-network-id",
|
Name: "filter-virtual-network-id",
|
||||||
Usage: "Show only routes that are attached to the given virtual network ID.",
|
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flags contains all filter flags.
|
// Flags contains all filter flags.
|
||||||
FilterFlags = []cli.Flag{
|
IpRouteFilterFlags = []cli.Flag{
|
||||||
&filterDeleted,
|
&filterIpRouteDeleted,
|
||||||
&filterTunnelID,
|
&filterIpRouteTunnelID,
|
||||||
&filterSubset,
|
&filterSubsetIpRoute,
|
||||||
&filterSuperset,
|
&filterSupersetIpRoute,
|
||||||
&filterComment,
|
&filterIpRouteComment,
|
||||||
&filterVnet,
|
&filterIpRouteByVnet,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter which routes get queried.
|
// IpRouteFilter which routes get queried.
|
||||||
type Filter struct {
|
type IpRouteFilter struct {
|
||||||
queryParams url.Values
|
queryParams url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromCLI parses CLI flags to discover which filters should get applied.
|
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||||
f := &Filter{
|
f := &IpRouteFilter{
|
||||||
queryParams: url.Values{},
|
queryParams: url.Values{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set deletion filter
|
// Set deletion filter
|
||||||
if flag := filterDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||||
f.deleted()
|
f.deleted()
|
||||||
} else {
|
} else {
|
||||||
f.notDeleted()
|
f.notDeleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
if subset, err := cidrFromFlag(c, filterSubset); err != nil {
|
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if subset != nil {
|
} else if subset != nil {
|
||||||
f.networkIsSupersetOf(*subset)
|
f.networkIsSupersetOf(*subset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if superset, err := cidrFromFlag(c, filterSuperset); err != nil {
|
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if superset != nil {
|
} else if superset != nil {
|
||||||
f.networkIsSupersetOf(*superset)
|
f.networkIsSupersetOf(*superset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment := c.String(filterComment.Name); comment != "" {
|
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||||
f.commentIs(comment)
|
f.commentIs(comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||||
u, err := uuid.Parse(tunnelID)
|
u, err := uuid.Parse(tunnelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterTunnelID.Name)
|
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||||
}
|
}
|
||||||
f.tunnelID(u)
|
f.tunnelID(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
if vnetId := c.String(filterVnet.Name); vnetId != "" {
|
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||||
u, err := uuid.Parse(vnetId)
|
u, err := uuid.Parse(vnetId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterVnet.Name)
|
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||||
}
|
}
|
||||||
f.vnetID(u)
|
f.vnetID(u)
|
||||||
}
|
}
|
||||||
|
@ -124,42 +124,42 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||||
return subset, nil
|
return subset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) commentIs(comment string) {
|
func (f *IpRouteFilter) commentIs(comment string) {
|
||||||
f.queryParams.Set("comment", comment)
|
f.queryParams.Set("comment", comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) notDeleted() {
|
func (f *IpRouteFilter) notDeleted() {
|
||||||
f.queryParams.Set("is_deleted", "false")
|
f.queryParams.Set("is_deleted", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) deleted() {
|
func (f *IpRouteFilter) deleted() {
|
||||||
f.queryParams.Set("is_deleted", "true")
|
f.queryParams.Set("is_deleted", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) networkIsSubsetOf(superset net.IPNet) {
|
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
||||||
f.queryParams.Set("network_subset", superset.String())
|
f.queryParams.Set("network_subset", superset.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) networkIsSupersetOf(subset net.IPNet) {
|
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
||||||
f.queryParams.Set("network_superset", subset.String())
|
f.queryParams.Set("network_superset", subset.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) existedAt(existedAt time.Time) {
|
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
||||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) tunnelID(id uuid.UUID) {
|
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
||||||
f.queryParams.Set("tunnel_id", id.String())
|
f.queryParams.Set("tunnel_id", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) vnetID(id uuid.UUID) {
|
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
||||||
f.queryParams.Set("virtual_network_id", id.String())
|
f.queryParams.Set("virtual_network_id", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) MaxFetchSize(max uint) {
|
func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Filter) Encode() string {
|
func (f IpRouteFilter) Encode() string {
|
||||||
return f.queryParams.Encode()
|
return f.queryParams.Encode()
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package teamnet
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
|
@ -0,0 +1,183 @@
|
||||||
|
package cfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||||
|
|
||||||
|
type Tunnel struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt time.Time `json:"deleted_at"`
|
||||||
|
Connections []Connection `json:"connections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
ColoName string `json:"colo_name"`
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||||
|
OriginIP net.IP `json:"origin_ip"`
|
||||||
|
OpenedAt time.Time `json:"opened_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveClient struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Features []string `json:"features"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
RunAt time.Time `json:"run_at"`
|
||||||
|
Connections []Connection `json:"conns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type newTunnel struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
TunnelSecret []byte `json:"tunnel_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CleanupParams struct {
|
||||||
|
queryParams url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCleanupParams() *CleanupParams {
|
||||||
|
return &CleanupParams{
|
||||||
|
queryParams: url.Values{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
||||||
|
cp.queryParams.Set("client_id", clientID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp CleanupParams) encode() string {
|
||||||
|
return cp.queryParams.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("tunnel name required")
|
||||||
|
}
|
||||||
|
if _, err := uuid.Parse(name); err == nil {
|
||||||
|
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
||||||
|
}
|
||||||
|
body := &newTunnel{
|
||||||
|
Name: name,
|
||||||
|
TunnelSecret: tunnelSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
return unmarshalTunnel(resp.Body)
|
||||||
|
case http.StatusConflict:
|
||||||
|
return nil, ErrTunnelNameConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("create tunnel", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return unmarshalTunnel(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("get tunnel", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||||
|
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return r.statusCodeToError("delete tunnel", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.RawQuery = filter.encode()
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseListTunnels(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||||
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseConnectionsDetails(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("list connection details", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||||
|
var clients []*ActiveClient
|
||||||
|
err := parseResponse(reader, &clients)
|
||||||
|
return clients, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
||||||
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
|
endpoint.RawQuery = params.encode()
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||||
|
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return r.statusCodeToError("cleanup connections", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||||
|
var tunnel Tunnel
|
||||||
|
err := parseResponse(reader, &tunnel)
|
||||||
|
return &tunnel, err
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package tunnelstore
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -12,44 +12,44 @@ const (
|
||||||
TimeLayout = time.RFC3339
|
TimeLayout = time.RFC3339
|
||||||
)
|
)
|
||||||
|
|
||||||
type Filter struct {
|
type TunnelFilter struct {
|
||||||
queryParams url.Values
|
queryParams url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFilter() *Filter {
|
func NewTunnelFilter() *TunnelFilter {
|
||||||
return &Filter{
|
return &TunnelFilter{
|
||||||
queryParams: url.Values{},
|
queryParams: url.Values{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ByName(name string) {
|
func (f *TunnelFilter) ByName(name string) {
|
||||||
f.queryParams.Set("name", name)
|
f.queryParams.Set("name", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ByNamePrefix(namePrefix string) {
|
func (f *TunnelFilter) ByNamePrefix(namePrefix string) {
|
||||||
f.queryParams.Set("name_prefix", namePrefix)
|
f.queryParams.Set("name_prefix", namePrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) {
|
func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||||
f.queryParams.Set("exclude_prefix", excludePrefix)
|
f.queryParams.Set("exclude_prefix", excludePrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) NoDeleted() {
|
func (f *TunnelFilter) NoDeleted() {
|
||||||
f.queryParams.Set("is_deleted", "false")
|
f.queryParams.Set("is_deleted", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ByExistedAt(existedAt time.Time) {
|
func (f *TunnelFilter) ByExistedAt(existedAt time.Time) {
|
||||||
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
|
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
|
func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) {
|
||||||
f.queryParams.Set("uuid", tunnelID.String())
|
f.queryParams.Set("uuid", tunnelID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) MaxFetchSize(max uint) {
|
func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Filter) encode() string {
|
func (f TunnelFilter) encode() string {
|
||||||
return f.queryParams.Encode()
|
return f.queryParams.Encode()
|
||||||
}
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
package cfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var loc, _ = time.LoadLocation("UTC")
|
||||||
|
|
||||||
|
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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "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 {
|
||||||
|
body string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *Tunnel
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty list",
|
||||||
|
args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`},
|
||||||
|
want: &Tunnel{
|
||||||
|
ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"),
|
||||||
|
Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV",
|
||||||
|
CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc),
|
||||||
|
DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc),
|
||||||
|
Connections: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := unmarshalTunnel(strings.NewReader(tt.args.body))
|
||||||
|
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": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalConnections(t *testing.T) {
|
||||||
|
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||||
|
expected := ActiveClient{
|
||||||
|
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||||
|
Features: []string{"ha-origin"},
|
||||||
|
Version: "2021.2.5",
|
||||||
|
Arch: "darwin_amd64",
|
||||||
|
RunAt: time.Time{},
|
||||||
|
Connections: []Connection{{
|
||||||
|
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||||
|
ColoName: "LIS",
|
||||||
|
IsPendingReconnect: false,
|
||||||
|
OriginIP: net.ParseIP("148.38.28.2"),
|
||||||
|
OpenedAt: time.Time{},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||||
|
}
|
|
@ -1,21 +1,59 @@
|
||||||
package tunnelstore
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/vnet"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
type NewVirtualNetwork struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualNetwork struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsDefault bool `json:"is_default_network"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt time.Time `json:"deleted_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateVirtualNetwork struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Comment *string `json:"comment,omitempty"`
|
||||||
|
IsDefault *bool `json:"is_default_network,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (virtualNetwork VirtualNetwork) TableString() string {
|
||||||
|
deletedColumn := "-"
|
||||||
|
if !virtualNetwork.DeletedAt.IsZero() {
|
||||||
|
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||||
|
virtualNetwork.ID,
|
||||||
|
virtualNetwork.Name,
|
||||||
|
strconv.FormatBool(virtualNetwork.IsDefault),
|
||||||
|
virtualNetwork.Comment,
|
||||||
|
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
||||||
|
deletedColumn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) {
|
||||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
return VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
@ -23,10 +61,10 @@ func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.
|
||||||
return parseVnet(resp.Body)
|
return parseVnet(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vnet.VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RESTClient) ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) {
|
||||||
endpoint := r.baseEndpoints.accountVnets
|
endpoint := r.baseEndpoints.accountVnets
|
||||||
endpoint.RawQuery = filter.Encode()
|
endpoint.RawQuery = filter.Encode()
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
|
@ -59,7 +97,7 @@ func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||||
return r.statusCodeToError("delete virtual network", resp)
|
return r.statusCodeToError("delete virtual network", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error {
|
||||||
endpoint := r.baseEndpoints.accountVnets
|
endpoint := r.baseEndpoints.accountVnets
|
||||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||||
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
||||||
|
@ -76,14 +114,14 @@ func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtu
|
||||||
return r.statusCodeToError("update virtual network", resp)
|
return r.statusCodeToError("update virtual network", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) {
|
func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) {
|
||||||
var vnets []*vnet.VirtualNetwork
|
var vnets []*VirtualNetwork
|
||||||
err := parseResponse(body, &vnets)
|
err := parseResponse(body, &vnets)
|
||||||
return vnets, err
|
return vnets, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) {
|
func parseVnet(body io.ReadCloser) (VirtualNetwork, error) {
|
||||||
var vnet vnet.VirtualNetwork
|
var vnet VirtualNetwork
|
||||||
err := parseResponse(body, &vnet)
|
err := parseResponse(body, &vnet)
|
||||||
return vnet, err
|
return vnet, err
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package vnet
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -10,68 +10,68 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
filterId = cli.StringFlag{
|
filterVnetId = cli.StringFlag{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
Usage: "List virtual networks with the given `ID`",
|
Usage: "List virtual networks with the given `ID`",
|
||||||
}
|
}
|
||||||
filterName = cli.StringFlag{
|
filterVnetByName = cli.StringFlag{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Usage: "List virtual networks with the given `NAME`",
|
Usage: "List virtual networks with the given `NAME`",
|
||||||
}
|
}
|
||||||
filterDefault = cli.BoolFlag{
|
filterDefaultVnet = cli.BoolFlag{
|
||||||
Name: "is-default",
|
Name: "is-default",
|
||||||
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
|
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
|
||||||
}
|
}
|
||||||
filterDeleted = cli.BoolFlag{
|
filterDeletedVnet = cli.BoolFlag{
|
||||||
Name: "show-deleted",
|
Name: "show-deleted",
|
||||||
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
|
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
|
||||||
}
|
}
|
||||||
FilterFlags = []cli.Flag{
|
VnetFilterFlags = []cli.Flag{
|
||||||
&filterId,
|
&filterVnetId,
|
||||||
&filterName,
|
&filterVnetByName,
|
||||||
&filterDefault,
|
&filterDefaultVnet,
|
||||||
&filterDeleted,
|
&filterDeletedVnet,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter which virtual networks get queried.
|
// VnetFilter which virtual networks get queried.
|
||||||
type Filter struct {
|
type VnetFilter struct {
|
||||||
queryParams url.Values
|
queryParams url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFilter() *Filter {
|
func NewVnetFilter() *VnetFilter {
|
||||||
return &Filter{
|
return &VnetFilter{
|
||||||
queryParams: url.Values{},
|
queryParams: url.Values{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ById(vnetId uuid.UUID) {
|
func (f *VnetFilter) ById(vnetId uuid.UUID) {
|
||||||
f.queryParams.Set("id", vnetId.String())
|
f.queryParams.Set("id", vnetId.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ByName(name string) {
|
func (f *VnetFilter) ByName(name string) {
|
||||||
f.queryParams.Set("name", name)
|
f.queryParams.Set("name", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) ByDefaultStatus(isDefault bool) {
|
func (f *VnetFilter) ByDefaultStatus(isDefault bool) {
|
||||||
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) WithDeleted(isDeleted bool) {
|
func (f *VnetFilter) WithDeleted(isDeleted bool) {
|
||||||
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) MaxFetchSize(max uint) {
|
func (f *VnetFilter) MaxFetchSize(max uint) {
|
||||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Filter) Encode() string {
|
func (f VnetFilter) Encode() string {
|
||||||
return f.queryParams.Encode()
|
return f.queryParams.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
|
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
|
||||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
func NewFromCLI(c *cli.Context) (*VnetFilter, error) {
|
||||||
f := NewFilter()
|
f := NewVnetFilter()
|
||||||
|
|
||||||
if id := c.String("id"); id != "" {
|
if id := c.String("id"); id != "" {
|
||||||
vnetId, err := uuid.Parse(id)
|
vnetId, err := uuid.Parse(id)
|
|
@ -1,4 +1,4 @@
|
||||||
package vnet
|
package cfapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||||
|
@ -35,7 +36,6 @@ import (
|
||||||
"github.com/cloudflare/cloudflared/signal"
|
"github.com/cloudflare/cloudflared/signal"
|
||||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||||
"github.com/cloudflare/cloudflared/tunneldns"
|
"github.com/cloudflare/cloudflared/tunneldns"
|
||||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -212,12 +212,12 @@ func runClassicTunnel(sc *subcommandContext) error {
|
||||||
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
|
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
|
func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
|
||||||
if hostname := c.String("hostname"); hostname != "" {
|
if hostname := c.String("hostname"); hostname != "" {
|
||||||
if lbPool := c.String("lb-pool"); lbPool != "" {
|
if lbPool := c.String("lb-pool"); lbPool != "" {
|
||||||
return tunnelstore.NewLBRoute(hostname, lbPool), true
|
return cfapi.NewLBRoute(hostname, lbPool), true
|
||||||
}
|
}
|
||||||
return tunnelstore.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Connectors []*tunnelstore.ActiveClient `json:"conns"`
|
Connectors []*cfapi.ActiveClient `json:"conns"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,9 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/certutil"
|
"github.com/cloudflare/cloudflared/certutil"
|
||||||
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
"github.com/cloudflare/cloudflared/connection"
|
"github.com/cloudflare/cloudflared/connection"
|
||||||
"github.com/cloudflare/cloudflared/logger"
|
"github.com/cloudflare/cloudflared/logger"
|
||||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type errInvalidJSONCredential struct {
|
type errInvalidJSONCredential struct {
|
||||||
|
@ -37,7 +37,7 @@ type subcommandContext struct {
|
||||||
fs fileSystem
|
fs fileSystem
|
||||||
|
|
||||||
// These fields should be accessed using their respective Getter
|
// These fields should be accessed using their respective Getter
|
||||||
tunnelstoreClient tunnelstore.Client
|
tunnelstoreClient cfapi.Client
|
||||||
userCredential *userCredential
|
userCredential *userCredential
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ type userCredential struct {
|
||||||
certPath string
|
certPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
func (sc *subcommandContext) client() (cfapi.Client, error) {
|
||||||
if sc.tunnelstoreClient != nil {
|
if sc.tunnelstoreClient != nil {
|
||||||
return sc.tunnelstoreClient, nil
|
return sc.tunnelstoreClient, nil
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
userAgent := fmt.Sprintf("cloudflared/%s", version)
|
userAgent := fmt.Sprintf("cloudflared/%s", version)
|
||||||
client, err := tunnelstore.NewRESTClient(
|
client, err := cfapi.NewRESTClient(
|
||||||
sc.c.String("api-url"),
|
sc.c.String("api-url"),
|
||||||
credential.cert.AccountID,
|
credential.cert.AccountID,
|
||||||
credential.cert.ZoneID,
|
credential.cert.ZoneID,
|
||||||
|
@ -149,7 +149,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
|
||||||
return credentials, nil
|
return credentials, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*tunnelstore.Tunnel, error) {
|
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
||||||
|
@ -224,7 +224,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||||
return tunnel, nil
|
return tunnel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
|
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -251,7 +251,7 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||||
}
|
}
|
||||||
if forceFlagSet {
|
if forceFlagSet {
|
||||||
if err := client.CleanupConnections(tunnel.ID, tunnelstore.NewCleanupParams()); err != nil {
|
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
||||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,7 +311,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||||
params := tunnelstore.NewCleanupParams()
|
params := cfapi.NewCleanupParams()
|
||||||
extraLog := ""
|
extraLog := ""
|
||||||
if connector := sc.c.String("connector-id"); connector != "" {
|
if connector := sc.c.String("connector-id"); connector != "" {
|
||||||
connectorID, err := uuid.Parse(connector)
|
connectorID, err := uuid.Parse(connector)
|
||||||
|
@ -335,7 +335,7 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
|
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -345,8 +345,8 @@ func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tun
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query Tunnelstore to find the active tunnel with the given name.
|
// Query Tunnelstore to find the active tunnel with the given name.
|
||||||
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
|
func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) {
|
||||||
filter := tunnelstore.NewFilter()
|
filter := cfapi.NewTunnelFilter()
|
||||||
filter.NoDeleted()
|
filter.NoDeleted()
|
||||||
filter.ByName(name)
|
filter.ByName(name)
|
||||||
tunnels, err := sc.list(filter)
|
tunnels, err := sc.list(filter)
|
||||||
|
@ -391,7 +391,7 @@ func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||||
uuids, names := splitUuids(inputs)
|
uuids, names := splitUuids(inputs)
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
filter := tunnelstore.NewFilter()
|
filter := cfapi.NewTunnelFilter()
|
||||||
filter.NoDeleted()
|
filter.NoDeleted()
|
||||||
filter.ByName(name)
|
filter.ByName(name)
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ package tunnel
|
||||||
import (
|
import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
const noClientMsg = "error while creating backend client"
|
const noClientMsg = "error while creating backend client"
|
||||||
|
|
||||||
func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
|
func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, noClientMsg)
|
return nil, errors.Wrap(err, noClientMsg)
|
||||||
|
@ -16,15 +16,15 @@ func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Deta
|
||||||
return client.ListRoutes(filter)
|
return client.ListRoutes(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return teamnet.Route{}, errors.Wrap(err, noClientMsg)
|
return cfapi.Route{}, errors.Wrap(err, noClientMsg)
|
||||||
}
|
}
|
||||||
return client.AddRoute(newRoute)
|
return client.AddRoute(newRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error {
|
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, noClientMsg)
|
return errors.Wrap(err, noClientMsg)
|
||||||
|
@ -32,10 +32,10 @@ func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error
|
||||||
return client.DeleteRoute(params)
|
return client.DeleteRoute(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) getRouteByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||||
}
|
}
|
||||||
return client.GetByIP(params)
|
return client.GetByIP(params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
"github.com/cloudflare/cloudflared/connection"
|
"github.com/cloudflare/cloudflared/connection"
|
||||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockFileSystem struct {
|
type mockFileSystem struct {
|
||||||
|
@ -36,7 +36,7 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||||
log *zerolog.Logger
|
log *zerolog.Logger
|
||||||
isUIEnabled bool
|
isUIEnabled bool
|
||||||
fs fileSystem
|
fs fileSystem
|
||||||
tunnelstoreClient tunnelstore.Client
|
tunnelstoreClient cfapi.Client
|
||||||
userCredential *userCredential
|
userCredential *userCredential
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
|
@ -187,13 +187,13 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type deleteMockTunnelStore struct {
|
type deleteMockTunnelStore struct {
|
||||||
tunnelstore.Client
|
cfapi.Client
|
||||||
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
||||||
deletedTunnelIDs []uuid.UUID
|
deletedTunnelIDs []uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockTunnelBehaviour struct {
|
type mockTunnelBehaviour struct {
|
||||||
tunnel tunnelstore.Tunnel
|
tunnel cfapi.Tunnel
|
||||||
deleteErr error
|
deleteErr error
|
||||||
cleanupErr error
|
cleanupErr error
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,7 @@ func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||||
tunnel, ok := d.mockTunnels[tunnelID]
|
tunnel, ok := d.mockTunnels[tunnelID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||||
|
@ -233,7 +233,7 @@ func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *tunnelstore.CleanupParams) error {
|
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error {
|
||||||
tunnel, ok := d.mockTunnels[tunnelID]
|
tunnel, ok := d.mockTunnels[tunnelID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||||
|
@ -284,10 +284,10 @@ func Test_subcommandContext_Delete(t *testing.T) {
|
||||||
}(),
|
}(),
|
||||||
tunnelstoreClient: newDeleteMockTunnelStore(
|
tunnelstoreClient: newDeleteMockTunnelStore(
|
||||||
mockTunnelBehaviour{
|
mockTunnelBehaviour{
|
||||||
tunnel: tunnelstore.Tunnel{ID: tunnelID1},
|
tunnel: cfapi.Tunnel{ID: tunnelID1},
|
||||||
},
|
},
|
||||||
mockTunnelBehaviour{
|
mockTunnelBehaviour{
|
||||||
tunnel: tunnelstore.Tunnel{ID: tunnelID2},
|
tunnel: cfapi.Tunnel{ID: tunnelID2},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,18 +4,18 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/vnet"
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (sc *subcommandContext) addVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
func (sc *subcommandContext) addVirtualNetwork(newVnet cfapi.NewVirtualNetwork) (cfapi.VirtualNetwork, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||||
}
|
}
|
||||||
return client.CreateVirtualNetwork(newVnet)
|
return client.CreateVirtualNetwork(newVnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) listVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*cfapi.VirtualNetwork, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, noClientMsg)
|
return nil, errors.Wrap(err, noClientMsg)
|
||||||
|
@ -31,7 +31,7 @@ func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
|
||||||
return client.DeleteVirtualNetwork(vnetId)
|
return client.DeleteVirtualNetwork(vnetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, noClientMsg)
|
return errors.Wrap(err, noClientMsg)
|
||||||
|
|
|
@ -21,11 +21,11 @@ import (
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||||
"github.com/cloudflare/cloudflared/config"
|
"github.com/cloudflare/cloudflared/config"
|
||||||
"github.com/cloudflare/cloudflared/connection"
|
"github.com/cloudflare/cloudflared/connection"
|
||||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -64,8 +64,8 @@ var (
|
||||||
Name: "when",
|
Name: "when",
|
||||||
Aliases: []string{"w"},
|
Aliases: []string{"w"},
|
||||||
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
|
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
|
||||||
Layout: tunnelstore.TimeLayout,
|
Layout: cfapi.TimeLayout,
|
||||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
|
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(cfapi.TimeLayout)),
|
||||||
}
|
}
|
||||||
listIDFlag = &cli.StringFlag{
|
listIDFlag = &cli.StringFlag{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
|
@ -260,7 +260,7 @@ func listCommand(c *cli.Context) error {
|
||||||
warningChecker := updater.StartWarningCheck(c)
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
defer warningChecker.LogWarningIfAny(sc.log)
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
filter := tunnelstore.NewFilter()
|
filter := cfapi.NewTunnelFilter()
|
||||||
if !c.Bool("show-deleted") {
|
if !c.Bool("show-deleted") {
|
||||||
filter.NoDeleted()
|
filter.NoDeleted()
|
||||||
}
|
}
|
||||||
|
@ -335,7 +335,7 @@ func listCommand(c *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) {
|
||||||
writer := tabWriter()
|
writer := tabWriter()
|
||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
|
@ -357,7 +357,7 @@ func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
|
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
|
||||||
|
|
||||||
// Count connections per colo
|
// Count connections per colo
|
||||||
numConnsPerColo := make(map[string]uint, len(connections))
|
numConnsPerColo := make(map[string]uint, len(connections))
|
||||||
|
@ -477,8 +477,8 @@ func tunnelInfo(c *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||||
filter := tunnelstore.NewFilter()
|
filter := cfapi.NewTunnelFilter()
|
||||||
filter.ByTunnelID(tunnelID)
|
filter.ByTunnelID(tunnelID)
|
||||||
tunnels, err := sc.list(filter)
|
tunnels, err := sc.list(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -711,7 +711,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||||
{
|
{
|
||||||
Name: "dns",
|
Name: "dns",
|
||||||
Action: cliutil.ConfiguredAction(routeDnsCommand),
|
Action: cliutil.ConfiguredAction(routeDnsCommand),
|
||||||
Usage: "Route a hostname by creating a DNS CNAME record to a tunnel",
|
Usage: "HostnameRoute a hostname by creating a DNS CNAME record to a tunnel",
|
||||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
|
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
|
||||||
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
|
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
|
||||||
Flags: []cli.Flag{overwriteDNSFlag},
|
Flags: []cli.Flag{overwriteDNSFlag},
|
||||||
|
@ -728,7 +728,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, error) {
|
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (cfapi.HostnameRoute, error) {
|
||||||
const (
|
const (
|
||||||
userHostnameIndex = 1
|
userHostnameIndex = 1
|
||||||
expectedNArgs = 2
|
expectedNArgs = 2
|
||||||
|
@ -742,10 +742,10 @@ func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route,
|
||||||
} else if !validateHostname(userHostname, true) {
|
} else if !validateHostname(userHostname, true) {
|
||||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||||
}
|
}
|
||||||
return tunnelstore.NewDNSRoute(userHostname, overwriteExisting), nil
|
return cfapi.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) {
|
||||||
const (
|
const (
|
||||||
lbNameIndex = 1
|
lbNameIndex = 1
|
||||||
lbPoolIndex = 2
|
lbPoolIndex = 2
|
||||||
|
@ -768,7 +768,7 @@ func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||||
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
|
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tunnelstore.NewLBRoute(lbName, lbPool), nil
|
return cfapi.NewLBRoute(lbName, lbPool), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
||||||
|
@ -815,7 +815,7 @@ func routeCommand(c *cli.Context, routeType string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var route tunnelstore.Route
|
var route cfapi.HostnameRoute
|
||||||
switch routeType {
|
switch routeType {
|
||||||
case "dns":
|
case "dns":
|
||||||
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))
|
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))
|
||||||
|
|
|
@ -8,12 +8,12 @@ import (
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_fmtConnections(t *testing.T) {
|
func Test_fmtConnections(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
connections []tunnelstore.Connection
|
connections []cfapi.Connection
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -23,14 +23,14 @@ func Test_fmtConnections(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
args: args{
|
args: args{
|
||||||
connections: []tunnelstore.Connection{},
|
connections: []cfapi.Connection{},
|
||||||
},
|
},
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "trivial",
|
name: "trivial",
|
||||||
args: args{
|
args: args{
|
||||||
connections: []tunnelstore.Connection{
|
connections: []cfapi.Connection{
|
||||||
{
|
{
|
||||||
ColoName: "DFW",
|
ColoName: "DFW",
|
||||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||||
|
@ -42,7 +42,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "with a pending reconnect",
|
name: "with a pending reconnect",
|
||||||
args: args{
|
args: args{
|
||||||
connections: []tunnelstore.Connection{
|
connections: []cfapi.Connection{
|
||||||
{
|
{
|
||||||
ColoName: "DFW",
|
ColoName: "DFW",
|
||||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||||
|
@ -55,7 +55,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "many colos",
|
name: "many colos",
|
||||||
args: args{
|
args: args{
|
||||||
connections: []tunnelstore.Connection{
|
connections: []cfapi.Connection{
|
||||||
{
|
{
|
||||||
ColoName: "YRV",
|
ColoName: "YRV",
|
||||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||||
|
|
|
@ -8,12 +8,11 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -92,7 +91,7 @@ to tell which virtual network whose routing table you want to use.`,
|
||||||
|
|
||||||
func showRoutesFlags() []cli.Flag {
|
func showRoutesFlags() []cli.Flag {
|
||||||
flags := make([]cli.Flag, 0)
|
flags := make([]cli.Flag, 0)
|
||||||
flags = append(flags, teamnet.FilterFlags...)
|
flags = append(flags, cfapi.IpRouteFilterFlags...)
|
||||||
flags = append(flags, outputFormatFlag)
|
flags = append(flags, outputFormatFlag)
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
@ -103,7 +102,7 @@ func showRoutesCommand(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filter, err := teamnet.NewFromCLI(c)
|
filter, err := cfapi.NewIpRouteFilterFromCLI(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "invalid config for routing filters")
|
return errors.Wrap(err, "invalid config for routing filters")
|
||||||
}
|
}
|
||||||
|
@ -168,7 +167,7 @@ func addRouteCommand(c *cli.Context) error {
|
||||||
vnetId = &id
|
vnetId = &id
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = sc.addRoute(teamnet.NewRoute{
|
_, err = sc.addRoute(cfapi.NewRoute{
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
Network: *network,
|
Network: *network,
|
||||||
TunnelID: tunnelID,
|
TunnelID: tunnelID,
|
||||||
|
@ -199,7 +198,7 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||||
return errors.New("Invalid network CIDR")
|
return errors.New("Invalid network CIDR")
|
||||||
}
|
}
|
||||||
|
|
||||||
params := teamnet.DeleteRouteParams{
|
params := cfapi.DeleteRouteParams{
|
||||||
Network: *network,
|
Network: *network,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +232,7 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||||
return fmt.Errorf("Invalid IP %s", ipInput)
|
return fmt.Errorf("Invalid IP %s", ipInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := teamnet.GetRouteByIpParams{
|
params := cfapi.GetRouteByIpParams{
|
||||||
Ip: ip,
|
Ip: ip,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,12 +251,12 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||||
if route.IsZero() {
|
if route.IsZero() {
|
||||||
fmt.Printf("No route matches the IP %s\n", ip)
|
fmt.Printf("No route matches the IP %s\n", ip)
|
||||||
} else {
|
} else {
|
||||||
formatAndPrintRouteList([]*teamnet.DetailedRoute{&route})
|
formatAndPrintRouteList([]*cfapi.DetailedRoute{&route})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
|
func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
|
||||||
const (
|
const (
|
||||||
minWidth = 0
|
minWidth = 0
|
||||||
tabWidth = 8
|
tabWidth = 8
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||||
"github.com/cloudflare/cloudflared/vnet"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -102,7 +102,7 @@ default or update an existing one to become the default.`,
|
||||||
|
|
||||||
func listVirtualNetworksFlags() []cli.Flag {
|
func listVirtualNetworksFlags() []cli.Flag {
|
||||||
flags := make([]cli.Flag, 0)
|
flags := make([]cli.Flag, 0)
|
||||||
flags = append(flags, vnet.FilterFlags...)
|
flags = append(flags, cfapi.VnetFilterFlags...)
|
||||||
flags = append(flags, outputFormatFlag)
|
flags = append(flags, outputFormatFlag)
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ func addVirtualNetworkCommand(c *cli.Context) error {
|
||||||
comment = args.Get(1)
|
comment = args.Get(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
newVnet := vnet.NewVirtualNetwork{
|
newVnet := cfapi.NewVirtualNetwork{
|
||||||
Name: name,
|
Name: name,
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
IsDefault: c.Bool(makeDefaultFlag.Name),
|
IsDefault: c.Bool(makeDefaultFlag.Name),
|
||||||
|
@ -160,7 +160,7 @@ func listVirtualNetworksCommand(c *cli.Context) error {
|
||||||
warningChecker := updater.StartWarningCheck(c)
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
defer warningChecker.LogWarningIfAny(sc.log)
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
filter, err := vnet.NewFromCLI(c)
|
filter, err := cfapi.NewFromCLI(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "invalid flags for filtering virtual networks")
|
return errors.Wrap(err, "invalid flags for filtering virtual networks")
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@ func updateVirtualNetworkCommand(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := vnet.UpdateVirtualNetwork{}
|
updates := cfapi.UpdateVirtualNetwork{}
|
||||||
|
|
||||||
if c.IsSet(newNameFlag.Name) {
|
if c.IsSet(newNameFlag.Name) {
|
||||||
newName := c.String(newNameFlag.Name)
|
newName := c.String(newNameFlag.Name)
|
||||||
|
@ -248,7 +248,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := vnet.NewFilter()
|
filter := cfapi.NewVnetFilter()
|
||||||
filter.WithDeleted(false)
|
filter.WithDeleted(false)
|
||||||
filter.ByName(input)
|
filter.ByName(input)
|
||||||
|
|
||||||
|
@ -264,7 +264,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||||
return vnets[0].ID, nil
|
return vnets[0].ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) {
|
func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) {
|
||||||
const (
|
const (
|
||||||
minWidth = 0
|
minWidth = 0
|
||||||
tabWidth = 8
|
tabWidth = 8
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package tunnelstore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CleanupParams struct {
|
|
||||||
queryParams url.Values
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCleanupParams() *CleanupParams {
|
|
||||||
return &CleanupParams{
|
|
||||||
queryParams: url.Values{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
|
||||||
cp.queryParams.Set("client_id", clientID.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp CleanupParams) encode() string {
|
|
||||||
return cp.queryParams.Encode()
|
|
||||||
}
|
|
|
@ -1,545 +0,0 @@
|
||||||
package tunnelstore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
|
||||||
"github.com/cloudflare/cloudflared/vnet"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultTimeout = 15 * time.Second
|
|
||||||
jsonContentType = "application/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
|
||||||
ErrBadRequest = errors.New("incorrect request parameters")
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
ErrAPINoSuccess = errors.New("API call failed")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Tunnel struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
DeletedAt time.Time `json:"deleted_at"`
|
|
||||||
Connections []Connection `json:"connections"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection struct {
|
|
||||||
ColoName string `json:"colo_name"`
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
|
||||||
OriginIP net.IP `json:"origin_ip"`
|
|
||||||
OpenedAt time.Time `json:"opened_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActiveClient struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Features []string `json:"features"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Arch string `json:"arch"`
|
|
||||||
RunAt time.Time `json:"run_at"`
|
|
||||||
Connections []Connection `json:"conns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Change = string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChangeNew = "new"
|
|
||||||
ChangeUpdated = "updated"
|
|
||||||
ChangeUnchanged = "unchanged"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Route represents a record type that can route to a tunnel
|
|
||||||
type Route interface {
|
|
||||||
json.Marshaler
|
|
||||||
RecordType() string
|
|
||||||
UnmarshalResult(body io.Reader) (RouteResult, error)
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouteResult interface {
|
|
||||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
|
||||||
SuccessSummary() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSRoute struct {
|
|
||||||
userHostname string
|
|
||||||
overwriteExisting bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSRouteResult struct {
|
|
||||||
route *DNSRoute
|
|
||||||
CName Change `json:"cname"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDNSRoute(userHostname string, overwriteExisting bool) Route {
|
|
||||||
return &DNSRoute{
|
|
||||||
userHostname: userHostname,
|
|
||||||
overwriteExisting: overwriteExisting,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
|
||||||
s := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
UserHostname string `json:"user_hostname"`
|
|
||||||
OverwriteExisting bool `json:"overwrite_existing"`
|
|
||||||
}{
|
|
||||||
Type: dr.RecordType(),
|
|
||||||
UserHostname: dr.userHostname,
|
|
||||||
OverwriteExisting: dr.overwriteExisting,
|
|
||||||
}
|
|
||||||
return json.Marshal(&s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
|
|
||||||
var result DNSRouteResult
|
|
||||||
err := parseResponse(body, &result)
|
|
||||||
result.route = dr
|
|
||||||
return &result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *DNSRoute) RecordType() string {
|
|
||||||
return "dns"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *DNSRoute) String() string {
|
|
||||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *DNSRouteResult) SuccessSummary() string {
|
|
||||||
var msgFmt string
|
|
||||||
switch res.CName {
|
|
||||||
case ChangeNew:
|
|
||||||
msgFmt = "Added CNAME %s which will route to this tunnel"
|
|
||||||
case ChangeUpdated: // this is not currently returned by tunnelsore
|
|
||||||
msgFmt = "%s updated to route to your tunnel"
|
|
||||||
case ChangeUnchanged:
|
|
||||||
msgFmt = "%s is already configured to route to your tunnel"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(msgFmt, res.hostname())
|
|
||||||
}
|
|
||||||
|
|
||||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
|
||||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
|
||||||
func (res *DNSRouteResult) hostname() string {
|
|
||||||
if res.Name != "" {
|
|
||||||
return res.Name
|
|
||||||
}
|
|
||||||
return res.route.userHostname
|
|
||||||
}
|
|
||||||
|
|
||||||
type LBRoute struct {
|
|
||||||
lbName string
|
|
||||||
lbPool string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LBRouteResult struct {
|
|
||||||
route *LBRoute
|
|
||||||
LoadBalancer Change `json:"load_balancer"`
|
|
||||||
Pool Change `json:"pool"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLBRoute(lbName, lbPool string) Route {
|
|
||||||
return &LBRoute{
|
|
||||||
lbName: lbName,
|
|
||||||
lbPool: lbPool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
|
||||||
s := struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
LBName string `json:"lb_name"`
|
|
||||||
LBPool string `json:"lb_pool"`
|
|
||||||
}{
|
|
||||||
Type: lr.RecordType(),
|
|
||||||
LBName: lr.lbName,
|
|
||||||
LBPool: lr.lbPool,
|
|
||||||
}
|
|
||||||
return json.Marshal(&s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lr *LBRoute) RecordType() string {
|
|
||||||
return "lb"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *LBRoute) String() string {
|
|
||||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
|
|
||||||
var result LBRouteResult
|
|
||||||
err := parseResponse(body, &result)
|
|
||||||
result.route = lr
|
|
||||||
return &result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *LBRouteResult) SuccessSummary() string {
|
|
||||||
var msg string
|
|
||||||
switch res.LoadBalancer + "," + res.Pool {
|
|
||||||
case "new,new":
|
|
||||||
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
|
||||||
case "new,updated":
|
|
||||||
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
|
||||||
case "new,unchanged":
|
|
||||||
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
|
||||||
case "updated,new":
|
|
||||||
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
|
||||||
case "updated,updated":
|
|
||||||
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
|
||||||
case "updated,unchanged":
|
|
||||||
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
|
||||||
case "unchanged,updated":
|
|
||||||
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
|
||||||
case "unchanged,unchanged":
|
|
||||||
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
|
||||||
case "unchanged,new":
|
|
||||||
// this state is not possible
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
// Named Tunnels endpoints
|
|
||||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
|
||||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
|
||||||
DeleteTunnel(tunnelID uuid.UUID) error
|
|
||||||
ListTunnels(filter *Filter) ([]*Tunnel, error)
|
|
||||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
|
||||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
|
||||||
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
|
|
||||||
|
|
||||||
// Teamnet endpoints
|
|
||||||
ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
|
|
||||||
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
|
||||||
DeleteRoute(params teamnet.DeleteRouteParams) error
|
|
||||||
GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error)
|
|
||||||
|
|
||||||
// Virtual Networks endpoints
|
|
||||||
CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error)
|
|
||||||
ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error)
|
|
||||||
DeleteVirtualNetwork(id uuid.UUID) error
|
|
||||||
UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type RESTClient struct {
|
|
||||||
baseEndpoints *baseEndpoints
|
|
||||||
authToken string
|
|
||||||
userAgent string
|
|
||||||
client http.Client
|
|
||||||
log *zerolog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
type baseEndpoints struct {
|
|
||||||
accountLevel url.URL
|
|
||||||
zoneLevel url.URL
|
|
||||||
accountRoutes url.URL
|
|
||||||
accountVnets url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Client = (*RESTClient)(nil)
|
|
||||||
|
|
||||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
|
||||||
if strings.HasSuffix(baseURL, "/") {
|
|
||||||
baseURL = baseURL[:len(baseURL)-1]
|
|
||||||
}
|
|
||||||
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
|
||||||
}
|
|
||||||
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
|
||||||
}
|
|
||||||
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
|
||||||
}
|
|
||||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
|
||||||
}
|
|
||||||
httpTransport := http.Transport{
|
|
||||||
TLSHandshakeTimeout: defaultTimeout,
|
|
||||||
ResponseHeaderTimeout: defaultTimeout,
|
|
||||||
}
|
|
||||||
http2.ConfigureTransport(&httpTransport)
|
|
||||||
return &RESTClient{
|
|
||||||
baseEndpoints: &baseEndpoints{
|
|
||||||
accountLevel: *accountLevelEndpoint,
|
|
||||||
zoneLevel: *zoneLevelEndpoint,
|
|
||||||
accountRoutes: *accountRoutesEndpoint,
|
|
||||||
accountVnets: *accountVnetsEndpoint,
|
|
||||||
},
|
|
||||||
authToken: authToken,
|
|
||||||
userAgent: userAgent,
|
|
||||||
client: http.Client{
|
|
||||||
Transport: &httpTransport,
|
|
||||||
Timeout: defaultTimeout,
|
|
||||||
},
|
|
||||||
log: log,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type newTunnel struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
TunnelSecret []byte `json:"tunnel_secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
|
|
||||||
if name == "" {
|
|
||||||
return nil, errors.New("tunnel name required")
|
|
||||||
}
|
|
||||||
if _, err := uuid.Parse(name); err == nil {
|
|
||||||
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
|
||||||
}
|
|
||||||
body := &newTunnel{
|
|
||||||
Name: name,
|
|
||||||
TunnelSecret: tunnelSecret,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusOK:
|
|
||||||
return unmarshalTunnel(resp.Body)
|
|
||||||
case http.StatusConflict:
|
|
||||||
return nil, ErrTunnelNameConflict
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, r.statusCodeToError("create tunnel", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return unmarshalTunnel(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, r.statusCodeToError("get tunnel", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
|
||||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return r.statusCodeToError("delete tunnel", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
|
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
|
||||||
endpoint.RawQuery = filter.encode()
|
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return parseListTunnels(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return parseConnectionsDetails(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, r.statusCodeToError("list connection details", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
|
||||||
var clients []*ActiveClient
|
|
||||||
err := parseResponse(reader, &clients)
|
|
||||||
return clients, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
|
||||||
endpoint.RawQuery = params.encode()
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
|
||||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return r.statusCodeToError("cleanup connections", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error) {
|
|
||||||
endpoint := r.baseEndpoints.zoneLevel
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
|
||||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return route.UnmarshalResult(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, r.statusCodeToError("add route", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
|
||||||
var bodyReader io.Reader
|
|
||||||
if body != nil {
|
|
||||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
|
||||||
} else {
|
|
||||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, url.String(), bodyReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", r.userAgent)
|
|
||||||
if bodyReader != nil {
|
|
||||||
req.Header.Set("Content-Type", jsonContentType)
|
|
||||||
}
|
|
||||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
|
||||||
req.Header.Add("Accept", "application/json;version=1")
|
|
||||||
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 response
|
|
||||||
// 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 := result.checkErrors(); 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.
|
|
||||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
|
||||||
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
|
||||||
var tunnel Tunnel
|
|
||||||
err := parseResponse(reader, &tunnel)
|
|
||||||
return &tunnel, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type response struct {
|
|
||||||
Success bool `json:"success,omitempty"`
|
|
||||||
Errors []apiErr `json:"errors,omitempty"`
|
|
||||||
Messages []string `json:"messages,omitempty"`
|
|
||||||
Result json.RawMessage `json:"result,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *response) checkErrors() error {
|
|
||||||
if len(r.Errors) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(r.Errors) == 1 {
|
|
||||||
return r.Errors[0]
|
|
||||||
}
|
|
||||||
var messages string
|
|
||||||
for _, e := range r.Errors {
|
|
||||||
messages += fmt.Sprintf("%s; ", e)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("API errors: %s", messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiErr struct {
|
|
||||||
Code json.Number `json:"code,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e apiErr) Error() string {
|
|
||||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
|
||||||
if resp.Header.Get("Content-Type") == "application/json" {
|
|
||||||
var errorsResp response
|
|
||||||
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
|
||||||
if err := errorsResp.checkErrors(); err != nil {
|
|
||||||
return errors.Errorf("Failed to %s: %s", op, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusOK:
|
|
||||||
return nil
|
|
||||||
case http.StatusBadRequest:
|
|
||||||
return ErrBadRequest
|
|
||||||
case http.StatusUnauthorized, http.StatusForbidden:
|
|
||||||
return ErrUnauthorized
|
|
||||||
case http.StatusNotFound:
|
|
||||||
return ErrNotFound
|
|
||||||
}
|
|
||||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
|
||||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
package tunnelstore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
|
||||||
func (r *RESTClient) ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
|
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
|
||||||
endpoint.RawQuery = filter.Encode()
|
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return parseListDetailedRoutes(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, r.statusCodeToError("list routes", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
|
||||||
func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
|
||||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
|
||||||
if err != nil {
|
|
||||||
return teamnet.Route{}, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return parseRoute(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return teamnet.Route{}, r.statusCodeToError("add route", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
|
||||||
func (r *RESTClient) DeleteRoute(params teamnet.DeleteRouteParams) error {
|
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
|
||||||
setVnetParam(&endpoint, params.VNetID)
|
|
||||||
|
|
||||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
_, err := parseRoute(resp.Body)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.statusCodeToError("delete route", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByIP checks which route will proxy a given IP.
|
|
||||||
func (r *RESTClient) GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
|
||||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
|
||||||
setVnetParam(&endpoint, params.VNetID)
|
|
||||||
|
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return teamnet.DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return parseDetailedRoute(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return teamnet.DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*teamnet.DetailedRoute, error) {
|
|
||||||
var routes []*teamnet.DetailedRoute
|
|
||||||
err := parseResponse(body, &routes)
|
|
||||||
return routes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRoute(body io.ReadCloser) (teamnet.Route, error) {
|
|
||||||
var route teamnet.Route
|
|
||||||
err := parseResponse(body, &route)
|
|
||||||
return route, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDetailedRoute(body io.ReadCloser) (teamnet.DetailedRoute, error) {
|
|
||||||
var route teamnet.DetailedRoute
|
|
||||||
err := parseResponse(body, &route)
|
|
||||||
return route, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// setVnetParam overwrites the URL's query parameters with a query param to scope the Route action to a certain
|
|
||||||
// virtual network (if one is provided).
|
|
||||||
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
|
||||||
queryParams := url.Values{}
|
|
||||||
if vnetID != nil {
|
|
||||||
queryParams.Set("virtual_network_id", vnetID.String())
|
|
||||||
}
|
|
||||||
endpoint.RawQuery = queryParams.Encode()
|
|
||||||
}
|
|
46
vnet/api.go
46
vnet/api.go
|
@ -1,46 +0,0 @@
|
||||||
package vnet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NewVirtualNetwork struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VirtualNetwork struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsDefault bool `json:"is_default_network"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
DeletedAt time.Time `json:"deleted_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateVirtualNetwork struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Comment *string `json:"comment,omitempty"`
|
|
||||||
IsDefault *bool `json:"is_default_network,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (virtualNetwork VirtualNetwork) TableString() string {
|
|
||||||
deletedColumn := "-"
|
|
||||||
if !virtualNetwork.DeletedAt.IsZero() {
|
|
||||||
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
|
||||||
virtualNetwork.ID,
|
|
||||||
virtualNetwork.Name,
|
|
||||||
strconv.FormatBool(virtualNetwork.IsDefault),
|
|
||||||
virtualNetwork.Comment,
|
|
||||||
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
|
||||||
deletedColumn,
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
Reference in New Issue