From 6822e4f8ab33a79e69c6627c414cb77bf788880a Mon Sep 17 00:00:00 2001 From: Nuno Diegues Date: Mon, 27 Dec 2021 14:56:50 +0000 Subject: [PATCH] TUN-5482: Refactor tunnelstore client related packages for more coherent package --- cfapi/base_client.go | 186 ++++++ cfapi/client.go | 39 ++ cfapi/hostname.go | 192 ++++++ .../client_test.go => cfapi/hostname_test.go | 132 +---- teamnet/api.go => cfapi/ip_route.go | 107 +++- teamnet/filter.go => cfapi/ip_route_filter.go | 74 +-- teamnet/api_test.go => cfapi/ip_route_test.go | 2 +- cfapi/tunnel.go | 183 ++++++ .../filter.go => cfapi/tunnel_filter.go | 24 +- cfapi/tunnel_test.go | 149 +++++ .../virtual_network.go | 62 +- .../virtual_network_filter.go | 44 +- .../virtual_network_test.go | 2 +- cmd/cloudflared/tunnel/cmd.go | 8 +- cmd/cloudflared/tunnel/info.go | 10 +- cmd/cloudflared/tunnel/subcommand_context.go | 24 +- .../tunnel/subcommand_context_teamnet.go | 14 +- .../tunnel/subcommand_context_test.go | 16 +- .../tunnel/subcommand_context_vnets.go | 10 +- cmd/cloudflared/tunnel/subcommands.go | 28 +- cmd/cloudflared/tunnel/subcommands_test.go | 12 +- cmd/cloudflared/tunnel/teamnet_subcommands.go | 19 +- cmd/cloudflared/tunnel/vnets_subcommands.go | 14 +- tunnelstore/cleanup_params.go | 25 - tunnelstore/client.go | 545 ------------------ tunnelstore/client_teamnet.go | 114 ---- vnet/api.go | 46 -- 27 files changed, 1056 insertions(+), 1025 deletions(-) create mode 100644 cfapi/base_client.go create mode 100644 cfapi/client.go create mode 100644 cfapi/hostname.go rename tunnelstore/client_test.go => cfapi/hostname_test.go (51%) rename teamnet/api.go => cfapi/ip_route.go (53%) rename teamnet/filter.go => cfapi/ip_route_filter.go (60%) rename teamnet/api_test.go => cfapi/ip_route_test.go (99%) create mode 100644 cfapi/tunnel.go rename tunnelstore/filter.go => cfapi/tunnel_filter.go (52%) create mode 100644 cfapi/tunnel_test.go rename tunnelstore/client_vnets.go => cfapi/virtual_network.go (52%) rename vnet/filter.go => cfapi/virtual_network_filter.go (68%) rename vnet/api_test.go => cfapi/virtual_network_test.go (99%) delete mode 100644 tunnelstore/cleanup_params.go delete mode 100644 tunnelstore/client.go delete mode 100644 tunnelstore/client_teamnet.go delete mode 100644 vnet/api.go diff --git a/cfapi/base_client.go b/cfapi/base_client.go new file mode 100644 index 00000000..42b99316 --- /dev/null +++ b/cfapi/base_client.go @@ -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)) +} diff --git a/cfapi/client.go b/cfapi/client.go new file mode 100644 index 00000000..b4f17927 --- /dev/null +++ b/cfapi/client.go @@ -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 +} diff --git a/cfapi/hostname.go b/cfapi/hostname.go new file mode 100644 index 00000000..b8ca8bd4 --- /dev/null +++ b/cfapi/hostname.go @@ -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) +} diff --git a/tunnelstore/client_test.go b/cfapi/hostname_test.go similarity index 51% rename from tunnelstore/client_test.go rename to cfapi/hostname_test.go index a3e0ec97..5100465a 100644 --- a/tunnelstore/client_test.go +++ b/cfapi/hostname_test.go @@ -1,17 +1,9 @@ -package tunnelstore +package cfapi import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net" - "reflect" "strings" "testing" - "time" - "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -105,125 +97,3 @@ func TestLBRouteResultSuccessSummary(t *testing.T) { 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) -} diff --git a/teamnet/api.go b/cfapi/ip_route.go similarity index 53% rename from teamnet/api.go rename to cfapi/ip_route.go index 61bdf48b..6749aa89 100644 --- a/teamnet/api.go +++ b/cfapi/ip_route.go @@ -1,9 +1,13 @@ -package teamnet +package cfapi import ( "encoding/json" "fmt" + "io" "net" + "net/http" + "net/url" + "path" "time" "github.com/google/uuid" @@ -133,3 +137,104 @@ type GetRouteByIpParams struct { // Optional field. If unset, backend will assume the default vnet for the account. 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() +} diff --git a/teamnet/filter.go b/cfapi/ip_route_filter.go similarity index 60% rename from teamnet/filter.go rename to cfapi/ip_route_filter.go index a9a7fdc3..1f0301ac 100644 --- a/teamnet/filter.go +++ b/cfapi/ip_route_filter.go @@ -1,4 +1,4 @@ -package teamnet +package cfapi import ( "fmt" @@ -13,90 +13,90 @@ import ( ) var ( - filterDeleted = cli.BoolFlag{ + filterIpRouteDeleted = cli.BoolFlag{ Name: "filter-is-deleted", 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", Usage: "Show only routes with the given tunnel ID.", } - filterSubset = cli.StringFlag{ + filterSubsetIpRoute = cli.StringFlag{ Name: "filter-network-is-subset-of", Aliases: []string{"nsub"}, 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", Aliases: []string{"nsup"}, Usage: "Show only routes whose network is a superset of the given network.", } - filterComment = cli.StringFlag{ + filterIpRouteComment = cli.StringFlag{ Name: "filter-comment-is", Usage: "Show only routes with this comment.", } - filterVnet = cli.StringFlag{ + filterIpRouteByVnet = cli.StringFlag{ Name: "filter-virtual-network-id", Usage: "Show only routes that are attached to the given virtual network ID.", } // Flags contains all filter flags. - FilterFlags = []cli.Flag{ - &filterDeleted, - &filterTunnelID, - &filterSubset, - &filterSuperset, - &filterComment, - &filterVnet, + IpRouteFilterFlags = []cli.Flag{ + &filterIpRouteDeleted, + &filterIpRouteTunnelID, + &filterSubsetIpRoute, + &filterSupersetIpRoute, + &filterIpRouteComment, + &filterIpRouteByVnet, } ) -// Filter which routes get queried. -type Filter struct { +// IpRouteFilter which routes get queried. +type IpRouteFilter struct { queryParams url.Values } -// NewFromCLI parses CLI flags to discover which filters should get applied. -func NewFromCLI(c *cli.Context) (*Filter, error) { - f := &Filter{ +// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied. +func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) { + f := &IpRouteFilter{ queryParams: url.Values{}, } // 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() } else { f.notDeleted() } - if subset, err := cidrFromFlag(c, filterSubset); err != nil { + if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil { return nil, err } else if subset != nil { f.networkIsSupersetOf(*subset) } - if superset, err := cidrFromFlag(c, filterSuperset); err != nil { + if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil { return nil, err } else if superset != nil { f.networkIsSupersetOf(*superset) } - if comment := c.String(filterComment.Name); comment != "" { + if comment := c.String(filterIpRouteComment.Name); comment != "" { f.commentIs(comment) } - if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" { + if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" { u, err := uuid.Parse(tunnelID) 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) } - if vnetId := c.String(filterVnet.Name); vnetId != "" { + if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" { u, err := uuid.Parse(vnetId) 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) } @@ -124,42 +124,42 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) { return subset, nil } -func (f *Filter) commentIs(comment string) { +func (f *IpRouteFilter) commentIs(comment string) { f.queryParams.Set("comment", comment) } -func (f *Filter) notDeleted() { +func (f *IpRouteFilter) notDeleted() { f.queryParams.Set("is_deleted", "false") } -func (f *Filter) deleted() { +func (f *IpRouteFilter) deleted() { 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()) } -func (f *Filter) networkIsSupersetOf(subset net.IPNet) { +func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) { 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)) } -func (f *Filter) tunnelID(id uuid.UUID) { +func (f *IpRouteFilter) tunnelID(id uuid.UUID) { 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()) } -func (f *Filter) MaxFetchSize(max uint) { +func (f *IpRouteFilter) MaxFetchSize(max uint) { f.queryParams.Set("per_page", strconv.Itoa(int(max))) } -func (f Filter) Encode() string { +func (f IpRouteFilter) Encode() string { return f.queryParams.Encode() } diff --git a/teamnet/api_test.go b/cfapi/ip_route_test.go similarity index 99% rename from teamnet/api_test.go rename to cfapi/ip_route_test.go index b2d54772..93fa45ea 100644 --- a/teamnet/api_test.go +++ b/cfapi/ip_route_test.go @@ -1,4 +1,4 @@ -package teamnet +package cfapi import ( "encoding/json" diff --git a/cfapi/tunnel.go b/cfapi/tunnel.go new file mode 100644 index 00000000..5d4ea298 --- /dev/null +++ b/cfapi/tunnel.go @@ -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 +} diff --git a/tunnelstore/filter.go b/cfapi/tunnel_filter.go similarity index 52% rename from tunnelstore/filter.go rename to cfapi/tunnel_filter.go index 97759b72..df8932bc 100644 --- a/tunnelstore/filter.go +++ b/cfapi/tunnel_filter.go @@ -1,4 +1,4 @@ -package tunnelstore +package cfapi import ( "net/url" @@ -12,44 +12,44 @@ const ( TimeLayout = time.RFC3339 ) -type Filter struct { +type TunnelFilter struct { queryParams url.Values } -func NewFilter() *Filter { - return &Filter{ +func NewTunnelFilter() *TunnelFilter { + return &TunnelFilter{ queryParams: url.Values{}, } } -func (f *Filter) ByName(name string) { +func (f *TunnelFilter) ByName(name string) { f.queryParams.Set("name", name) } -func (f *Filter) ByNamePrefix(namePrefix string) { +func (f *TunnelFilter) ByNamePrefix(namePrefix string) { f.queryParams.Set("name_prefix", namePrefix) } -func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) { +func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) { f.queryParams.Set("exclude_prefix", excludePrefix) } -func (f *Filter) NoDeleted() { +func (f *TunnelFilter) NoDeleted() { 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)) } -func (f *Filter) ByTunnelID(tunnelID uuid.UUID) { +func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) { 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))) } -func (f Filter) encode() string { +func (f TunnelFilter) encode() string { return f.queryParams.Encode() } diff --git a/cfapi/tunnel_test.go b/cfapi/tunnel_test.go new file mode 100644 index 00000000..fbb5fba8 --- /dev/null +++ b/cfapi/tunnel_test.go @@ -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) +} diff --git a/tunnelstore/client_vnets.go b/cfapi/virtual_network.go similarity index 52% rename from tunnelstore/client_vnets.go rename to cfapi/virtual_network.go index ab83c617..e346678a 100644 --- a/tunnelstore/client_vnets.go +++ b/cfapi/virtual_network.go @@ -1,21 +1,59 @@ -package tunnelstore +package cfapi import ( + "fmt" "io" "net/http" "net/url" "path" + "strconv" + "time" "github.com/google/uuid" "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) if err != nil { - return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed") + return VirtualNetwork{}, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() @@ -23,10 +61,10 @@ func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet. 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.RawQuery = filter.Encode() 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) } -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.Path = path.Join(endpoint.Path, url.PathEscape(id.String())) 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) } -func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) { - var vnets []*vnet.VirtualNetwork +func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) { + var vnets []*VirtualNetwork err := parseResponse(body, &vnets) return vnets, err } -func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) { - var vnet vnet.VirtualNetwork +func parseVnet(body io.ReadCloser) (VirtualNetwork, error) { + var vnet VirtualNetwork err := parseResponse(body, &vnet) return vnet, err } diff --git a/vnet/filter.go b/cfapi/virtual_network_filter.go similarity index 68% rename from vnet/filter.go rename to cfapi/virtual_network_filter.go index 5cd7d82e..ddba442d 100644 --- a/vnet/filter.go +++ b/cfapi/virtual_network_filter.go @@ -1,4 +1,4 @@ -package vnet +package cfapi import ( "net/url" @@ -10,68 +10,68 @@ import ( ) var ( - filterId = cli.StringFlag{ + filterVnetId = cli.StringFlag{ Name: "id", Usage: "List virtual networks with the given `ID`", } - filterName = cli.StringFlag{ + filterVnetByName = cli.StringFlag{ Name: "name", Usage: "List virtual networks with the given `NAME`", } - filterDefault = cli.BoolFlag{ + filterDefaultVnet = cli.BoolFlag{ 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.", } - filterDeleted = cli.BoolFlag{ + filterDeletedVnet = cli.BoolFlag{ Name: "show-deleted", Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.", } - FilterFlags = []cli.Flag{ - &filterId, - &filterName, - &filterDefault, - &filterDeleted, + VnetFilterFlags = []cli.Flag{ + &filterVnetId, + &filterVnetByName, + &filterDefaultVnet, + &filterDeletedVnet, } ) -// Filter which virtual networks get queried. -type Filter struct { +// VnetFilter which virtual networks get queried. +type VnetFilter struct { queryParams url.Values } -func NewFilter() *Filter { - return &Filter{ +func NewVnetFilter() *VnetFilter { + return &VnetFilter{ queryParams: url.Values{}, } } -func (f *Filter) ById(vnetId uuid.UUID) { +func (f *VnetFilter) ById(vnetId uuid.UUID) { f.queryParams.Set("id", vnetId.String()) } -func (f *Filter) ByName(name string) { +func (f *VnetFilter) ByName(name string) { 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)) } -func (f *Filter) WithDeleted(isDeleted bool) { +func (f *VnetFilter) WithDeleted(isDeleted bool) { 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))) } -func (f Filter) Encode() string { +func (f VnetFilter) Encode() string { return f.queryParams.Encode() } // NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks. -func NewFromCLI(c *cli.Context) (*Filter, error) { - f := NewFilter() +func NewFromCLI(c *cli.Context) (*VnetFilter, error) { + f := NewVnetFilter() if id := c.String("id"); id != "" { vnetId, err := uuid.Parse(id) diff --git a/vnet/api_test.go b/cfapi/virtual_network_test.go similarity index 99% rename from vnet/api_test.go rename to cfapi/virtual_network_test.go index 76e35715..528922c2 100644 --- a/vnet/api_test.go +++ b/cfapi/virtual_network_test.go @@ -1,4 +1,4 @@ -package vnet +package cfapi import ( "encoding/json" diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index d3c4aabe..0eb3ac75 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -21,6 +21,7 @@ import ( "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" + "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns" @@ -35,7 +36,6 @@ import ( "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tunneldns" - "github.com/cloudflare/cloudflared/tunnelstore" ) const ( @@ -212,12 +212,12 @@ func runClassicTunnel(sc *subcommandContext) error { 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 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 } diff --git a/cmd/cloudflared/tunnel/info.go b/cmd/cloudflared/tunnel/info.go index d3610f46..7c20556a 100644 --- a/cmd/cloudflared/tunnel/info.go +++ b/cmd/cloudflared/tunnel/info.go @@ -5,12 +5,12 @@ import ( "github.com/google/uuid" - "github.com/cloudflare/cloudflared/tunnelstore" + "github.com/cloudflare/cloudflared/cfapi" ) type Info struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt"` - Connectors []*tunnelstore.ActiveClient `json:"conns"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + Connectors []*cfapi.ActiveClient `json:"conns"` } diff --git a/cmd/cloudflared/tunnel/subcommand_context.go b/cmd/cloudflared/tunnel/subcommand_context.go index 41f0a358..2a20aaaa 100644 --- a/cmd/cloudflared/tunnel/subcommand_context.go +++ b/cmd/cloudflared/tunnel/subcommand_context.go @@ -14,9 +14,9 @@ import ( "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/certutil" + "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/logger" - "github.com/cloudflare/cloudflared/tunnelstore" ) type errInvalidJSONCredential struct { @@ -37,7 +37,7 @@ type subcommandContext struct { fs fileSystem // These fields should be accessed using their respective Getter - tunnelstoreClient tunnelstore.Client + tunnelstoreClient cfapi.Client userCredential *userCredential } @@ -68,7 +68,7 @@ type userCredential struct { certPath string } -func (sc *subcommandContext) client() (tunnelstore.Client, error) { +func (sc *subcommandContext) client() (cfapi.Client, error) { if sc.tunnelstoreClient != nil { return sc.tunnelstoreClient, nil } @@ -77,7 +77,7 @@ func (sc *subcommandContext) client() (tunnelstore.Client, error) { return nil, err } userAgent := fmt.Sprintf("cloudflared/%s", version) - client, err := tunnelstore.NewRESTClient( + client, err := cfapi.NewRESTClient( sc.c.String("api-url"), credential.cert.AccountID, credential.cert.ZoneID, @@ -149,7 +149,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne 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() if err != nil { 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 } -func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) { +func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) { client, err := sc.client() if err != nil { 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) } 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) } } @@ -311,7 +311,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error { } func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error { - params := tunnelstore.NewCleanupParams() + params := cfapi.NewCleanupParams() extraLog := "" if connector := sc.c.String("connector-id"); connector != "" { connectorID, err := uuid.Parse(connector) @@ -335,7 +335,7 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error { 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() if err != nil { 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. -func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) { - filter := tunnelstore.NewFilter() +func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) { + filter := cfapi.NewTunnelFilter() filter.NoDeleted() filter.ByName(name) tunnels, err := sc.list(filter) @@ -391,7 +391,7 @@ func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) { uuids, names := splitUuids(inputs) for _, name := range names { - filter := tunnelstore.NewFilter() + filter := cfapi.NewTunnelFilter() filter.NoDeleted() filter.ByName(name) diff --git a/cmd/cloudflared/tunnel/subcommand_context_teamnet.go b/cmd/cloudflared/tunnel/subcommand_context_teamnet.go index 22e6b9f2..4605172c 100644 --- a/cmd/cloudflared/tunnel/subcommand_context_teamnet.go +++ b/cmd/cloudflared/tunnel/subcommand_context_teamnet.go @@ -3,12 +3,12 @@ package tunnel import ( "github.com/pkg/errors" - "github.com/cloudflare/cloudflared/teamnet" + "github.com/cloudflare/cloudflared/cfapi" ) 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() if err != nil { return nil, errors.Wrap(err, noClientMsg) @@ -16,15 +16,15 @@ func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Deta 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() if err != nil { - return teamnet.Route{}, errors.Wrap(err, noClientMsg) + return cfapi.Route{}, errors.Wrap(err, noClientMsg) } return client.AddRoute(newRoute) } -func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error { +func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error { client, err := sc.client() if err != nil { return errors.Wrap(err, noClientMsg) @@ -32,10 +32,10 @@ func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error 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() if err != nil { - return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg) + return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg) } return client.GetByIP(params) } diff --git a/cmd/cloudflared/tunnel/subcommand_context_test.go b/cmd/cloudflared/tunnel/subcommand_context_test.go index cb9a9512..61a1e68b 100644 --- a/cmd/cloudflared/tunnel/subcommand_context_test.go +++ b/cmd/cloudflared/tunnel/subcommand_context_test.go @@ -13,8 +13,8 @@ import ( "github.com/rs/zerolog" "github.com/urfave/cli/v2" + "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/connection" - "github.com/cloudflare/cloudflared/tunnelstore" ) type mockFileSystem struct { @@ -36,7 +36,7 @@ func Test_subcommandContext_findCredentials(t *testing.T) { log *zerolog.Logger isUIEnabled bool fs fileSystem - tunnelstoreClient tunnelstore.Client + tunnelstoreClient cfapi.Client userCredential *userCredential } type args struct { @@ -187,13 +187,13 @@ func Test_subcommandContext_findCredentials(t *testing.T) { } type deleteMockTunnelStore struct { - tunnelstore.Client + cfapi.Client mockTunnels map[uuid.UUID]mockTunnelBehaviour deletedTunnelIDs []uuid.UUID } type mockTunnelBehaviour struct { - tunnel tunnelstore.Tunnel + tunnel cfapi.Tunnel deleteErr 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] if !ok { return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID) @@ -233,7 +233,7 @@ func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error { 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] if !ok { return fmt.Errorf("Couldn't find tunnel: %v", tunnelID) @@ -284,10 +284,10 @@ func Test_subcommandContext_Delete(t *testing.T) { }(), tunnelstoreClient: newDeleteMockTunnelStore( mockTunnelBehaviour{ - tunnel: tunnelstore.Tunnel{ID: tunnelID1}, + tunnel: cfapi.Tunnel{ID: tunnelID1}, }, mockTunnelBehaviour{ - tunnel: tunnelstore.Tunnel{ID: tunnelID2}, + tunnel: cfapi.Tunnel{ID: tunnelID2}, }, ), }, diff --git a/cmd/cloudflared/tunnel/subcommand_context_vnets.go b/cmd/cloudflared/tunnel/subcommand_context_vnets.go index 27bdc71a..14e055fe 100644 --- a/cmd/cloudflared/tunnel/subcommand_context_vnets.go +++ b/cmd/cloudflared/tunnel/subcommand_context_vnets.go @@ -4,18 +4,18 @@ import ( "github.com/google/uuid" "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() if err != nil { - return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg) + return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg) } 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() if err != nil { return nil, errors.Wrap(err, noClientMsg) @@ -31,7 +31,7 @@ func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error { 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() if err != nil { return errors.Wrap(err, noClientMsg) diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go index ff18298e..22dca2a4 100644 --- a/cmd/cloudflared/tunnel/subcommands.go +++ b/cmd/cloudflared/tunnel/subcommands.go @@ -21,11 +21,11 @@ import ( "golang.org/x/net/idna" yaml "gopkg.in/yaml.v2" + "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" - "github.com/cloudflare/cloudflared/tunnelstore" ) const ( @@ -64,8 +64,8 @@ var ( Name: "when", Aliases: []string{"w"}, Usage: "List tunnels that are active at the given `TIME` in RFC3339 format", - Layout: tunnelstore.TimeLayout, - DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)), + Layout: cfapi.TimeLayout, + DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(cfapi.TimeLayout)), } listIDFlag = &cli.StringFlag{ Name: "id", @@ -260,7 +260,7 @@ func listCommand(c *cli.Context) error { warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) - filter := tunnelstore.NewFilter() + filter := cfapi.NewTunnelFilter() if !c.Bool("show-deleted") { filter.NoDeleted() } @@ -335,7 +335,7 @@ func listCommand(c *cli.Context) error { return nil } -func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) { +func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) { writer := tabWriter() 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 numConnsPerColo := make(map[string]uint, len(connections)) @@ -477,8 +477,8 @@ func tunnelInfo(c *cli.Context) error { return nil } -func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) { - filter := tunnelstore.NewFilter() +func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error) { + filter := cfapi.NewTunnelFilter() filter.ByTunnelID(tunnelID) tunnels, err := sc.list(filter) if err != nil { @@ -711,7 +711,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava { Name: "dns", 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]", Description: `Creates a DNS CNAME record hostname that points to the tunnel.`, 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 ( userHostnameIndex = 1 expectedNArgs = 2 @@ -742,10 +742,10 @@ func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, } else if !validateHostname(userHostname, true) { 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 ( lbNameIndex = 1 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 tunnelstore.NewLBRoute(lbName, lbPool), nil + return cfapi.NewLBRoute(lbName, lbPool), nil } 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 { return err } - var route tunnelstore.Route + var route cfapi.HostnameRoute switch routeType { case "dns": route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName)) diff --git a/cmd/cloudflared/tunnel/subcommands_test.go b/cmd/cloudflared/tunnel/subcommands_test.go index 67aad6ea..4ebbd922 100644 --- a/cmd/cloudflared/tunnel/subcommands_test.go +++ b/cmd/cloudflared/tunnel/subcommands_test.go @@ -8,12 +8,12 @@ import ( homedir "github.com/mitchellh/go-homedir" "github.com/stretchr/testify/assert" - "github.com/cloudflare/cloudflared/tunnelstore" + "github.com/cloudflare/cloudflared/cfapi" ) func Test_fmtConnections(t *testing.T) { type args struct { - connections []tunnelstore.Connection + connections []cfapi.Connection } tests := []struct { name string @@ -23,14 +23,14 @@ func Test_fmtConnections(t *testing.T) { { name: "empty", args: args{ - connections: []tunnelstore.Connection{}, + connections: []cfapi.Connection{}, }, want: "", }, { name: "trivial", args: args{ - connections: []tunnelstore.Connection{ + connections: []cfapi.Connection{ { ColoName: "DFW", ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), @@ -42,7 +42,7 @@ func Test_fmtConnections(t *testing.T) { { name: "with a pending reconnect", args: args{ - connections: []tunnelstore.Connection{ + connections: []cfapi.Connection{ { ColoName: "DFW", ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), @@ -55,7 +55,7 @@ func Test_fmtConnections(t *testing.T) { { name: "many colos", args: args{ - connections: []tunnelstore.Connection{ + connections: []cfapi.Connection{ { ColoName: "YRV", ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), diff --git a/cmd/cloudflared/tunnel/teamnet_subcommands.go b/cmd/cloudflared/tunnel/teamnet_subcommands.go index 2e8174c6..fbcec009 100644 --- a/cmd/cloudflared/tunnel/teamnet_subcommands.go +++ b/cmd/cloudflared/tunnel/teamnet_subcommands.go @@ -8,12 +8,11 @@ import ( "github.com/google/uuid" "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/updater" - "github.com/cloudflare/cloudflared/teamnet" - - "github.com/urfave/cli/v2" ) var ( @@ -92,7 +91,7 @@ to tell which virtual network whose routing table you want to use.`, func showRoutesFlags() []cli.Flag { flags := make([]cli.Flag, 0) - flags = append(flags, teamnet.FilterFlags...) + flags = append(flags, cfapi.IpRouteFilterFlags...) flags = append(flags, outputFormatFlag) return flags } @@ -103,7 +102,7 @@ func showRoutesCommand(c *cli.Context) error { return err } - filter, err := teamnet.NewFromCLI(c) + filter, err := cfapi.NewIpRouteFilterFromCLI(c) if err != nil { return errors.Wrap(err, "invalid config for routing filters") } @@ -168,7 +167,7 @@ func addRouteCommand(c *cli.Context) error { vnetId = &id } - _, err = sc.addRoute(teamnet.NewRoute{ + _, err = sc.addRoute(cfapi.NewRoute{ Comment: comment, Network: *network, TunnelID: tunnelID, @@ -199,7 +198,7 @@ func deleteRouteCommand(c *cli.Context) error { return errors.New("Invalid network CIDR") } - params := teamnet.DeleteRouteParams{ + params := cfapi.DeleteRouteParams{ Network: *network, } @@ -233,7 +232,7 @@ func getRouteByIPCommand(c *cli.Context) error { return fmt.Errorf("Invalid IP %s", ipInput) } - params := teamnet.GetRouteByIpParams{ + params := cfapi.GetRouteByIpParams{ Ip: ip, } @@ -252,12 +251,12 @@ func getRouteByIPCommand(c *cli.Context) error { if route.IsZero() { fmt.Printf("No route matches the IP %s\n", ip) } else { - formatAndPrintRouteList([]*teamnet.DetailedRoute{&route}) + formatAndPrintRouteList([]*cfapi.DetailedRoute{&route}) } return nil } -func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) { +func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) { const ( minWidth = 0 tabWidth = 8 diff --git a/cmd/cloudflared/tunnel/vnets_subcommands.go b/cmd/cloudflared/tunnel/vnets_subcommands.go index 939b4af6..e55327ce 100644 --- a/cmd/cloudflared/tunnel/vnets_subcommands.go +++ b/cmd/cloudflared/tunnel/vnets_subcommands.go @@ -9,9 +9,9 @@ import ( "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/updater" - "github.com/cloudflare/cloudflared/vnet" ) var ( @@ -102,7 +102,7 @@ default or update an existing one to become the default.`, func listVirtualNetworksFlags() []cli.Flag { flags := make([]cli.Flag, 0) - flags = append(flags, vnet.FilterFlags...) + flags = append(flags, cfapi.VnetFilterFlags...) flags = append(flags, outputFormatFlag) return flags } @@ -128,7 +128,7 @@ func addVirtualNetworkCommand(c *cli.Context) error { comment = args.Get(1) } - newVnet := vnet.NewVirtualNetwork{ + newVnet := cfapi.NewVirtualNetwork{ Name: name, Comment: comment, IsDefault: c.Bool(makeDefaultFlag.Name), @@ -160,7 +160,7 @@ func listVirtualNetworksCommand(c *cli.Context) error { warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) - filter, err := vnet.NewFromCLI(c) + filter, err := cfapi.NewFromCLI(c) if err != nil { return errors.Wrap(err, "invalid flags for filtering virtual networks") } @@ -220,7 +220,7 @@ func updateVirtualNetworkCommand(c *cli.Context) error { return err } - updates := vnet.UpdateVirtualNetwork{} + updates := cfapi.UpdateVirtualNetwork{} if c.IsSet(newNameFlag.Name) { newName := c.String(newNameFlag.Name) @@ -248,7 +248,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) { return val, nil } - filter := vnet.NewFilter() + filter := cfapi.NewVnetFilter() filter.WithDeleted(false) filter.ByName(input) @@ -264,7 +264,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) { return vnets[0].ID, nil } -func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) { +func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) { const ( minWidth = 0 tabWidth = 8 diff --git a/tunnelstore/cleanup_params.go b/tunnelstore/cleanup_params.go deleted file mode 100644 index 4b24ba35..00000000 --- a/tunnelstore/cleanup_params.go +++ /dev/null @@ -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() -} diff --git a/tunnelstore/client.go b/tunnelstore/client.go deleted file mode 100644 index b8434b9f..00000000 --- a/tunnelstore/client.go +++ /dev/null @@ -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)) -} diff --git a/tunnelstore/client_teamnet.go b/tunnelstore/client_teamnet.go deleted file mode 100644 index b6505945..00000000 --- a/tunnelstore/client_teamnet.go +++ /dev/null @@ -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() -} diff --git a/vnet/api.go b/vnet/api.go deleted file mode 100644 index 3ce38d4f..00000000 --- a/vnet/api.go +++ /dev/null @@ -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, - ) -}