TUN-3669: Teamnet commands to add/show Teamnet routes.
This commit is contained in:
parent
2ea491b1d0
commit
94c639d225
|
@ -0,0 +1,21 @@
|
||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Route, error) {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.ListRoutes(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return teamnet.Route{}, err
|
||||||
|
}
|
||||||
|
return client.AddRoute(newRoute)
|
||||||
|
}
|
|
@ -203,14 +203,14 @@ func listCommand(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tunnels) > 0 {
|
if len(tunnels) > 0 {
|
||||||
fmtAndPrintTunnelList(tunnels, c.Bool("show-recently-disconnected"))
|
formatAndPrintTunnelList(tunnels, c.Bool("show-recently-disconnected"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
|
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
||||||
const (
|
const (
|
||||||
minWidth = 0
|
minWidth = 0
|
||||||
tabWidth = 8
|
tabWidth = 8
|
||||||
|
@ -407,6 +407,9 @@ func buildRouteCommand() *cli.Command {
|
||||||
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
|
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
|
||||||
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
|
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
|
||||||
CustomHelpTemplate: commandHelpTemplate(),
|
CustomHelpTemplate: commandHelpTemplate(),
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
buildRouteIPSubcommand(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildRouteIPSubcommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "ip",
|
||||||
|
Category: "Tunnel",
|
||||||
|
Usage: "Configure and query Cloudflare for Teams private routes",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
|
||||||
|
Hidden: true,
|
||||||
|
Description: `cloudflared lets you provision private Cloudflare for Teams routes to origins in your corporate
|
||||||
|
network, so that you can ensure the only people who can access your private IP subnets are people using a
|
||||||
|
corporate device enrolled in Cloudflare for Teams.
|
||||||
|
`,
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
Action: cliutil.ErrorHandler(addRouteCommand),
|
||||||
|
Usage: "Add a new Teamnet route to the table",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [CIDR] [TUNNEL] [COMMENT?]",
|
||||||
|
Description: `Add a new Cloudflare for Teams private route from a given tunnel (identified by name or
|
||||||
|
UUID) to a given IP network in your private IP space. This route will go through your Gateway rules.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "show",
|
||||||
|
Action: cliutil.ErrorHandler(showRoutesCommand),
|
||||||
|
Usage: "Show the routing table",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]",
|
||||||
|
Description: `Shows all Cloudflare for Teams private routes. Using flags to specify filters means that
|
||||||
|
only routes which match that filter get shown.`,
|
||||||
|
Flags: teamnet.Flags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showRoutesCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := teamnet.NewFromCLI(c)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid config for routing filters")
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := sc.listRoutes(filter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||||
|
return renderOutput(outputFormat, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(routes) > 0 {
|
||||||
|
formatAndPrintRouteList(routes)
|
||||||
|
} else {
|
||||||
|
fmt.Println("You have no routes, use 'cloudflared tunnel route ip add' to add a route")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRouteCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.NArg() < 2 {
|
||||||
|
return fmt.Errorf("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with")
|
||||||
|
}
|
||||||
|
args := c.Args()
|
||||||
|
_, network, err := net.ParseCIDR(args.Get(0))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Invalid network CIDR")
|
||||||
|
}
|
||||||
|
if network == nil {
|
||||||
|
return errors.New("Invalid network CIDR")
|
||||||
|
}
|
||||||
|
tunnelRef := args.Get(1)
|
||||||
|
tunnelID, err := sc.findID(tunnelRef)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Invalid tunnel")
|
||||||
|
}
|
||||||
|
comment := ""
|
||||||
|
if c.NArg() >= 3 {
|
||||||
|
comment = args.Get(2)
|
||||||
|
}
|
||||||
|
_, err = sc.addRoute(teamnet.NewRoute{
|
||||||
|
Comment: comment,
|
||||||
|
Network: *network,
|
||||||
|
TunnelID: tunnelID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "API error")
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully added route for %s over tunnel %s\n", network, tunnelID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAndPrintRouteList(routes []*teamnet.Route) {
|
||||||
|
const (
|
||||||
|
minWidth = 0
|
||||||
|
tabWidth = 8
|
||||||
|
padding = 1
|
||||||
|
padChar = ' '
|
||||||
|
flags = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
// Print column headers with tabbed columns
|
||||||
|
_, _ = fmt.Fprintln(writer, "NETWORK\tCOMMENT\tTUNNEL ID\tCREATED\tDELETED\t")
|
||||||
|
|
||||||
|
// Loop through routes, create formatted string for each, and print using tabwriter
|
||||||
|
for _, route := range routes {
|
||||||
|
formattedStr := route.TableString()
|
||||||
|
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package teamnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Route is a mapping from customer's IP space to a tunnel.
|
||||||
|
// Each route allows the customer to route eyeballs in their corporate network
|
||||||
|
// to certain private IP ranges. Each Route represents an IP range in their
|
||||||
|
// network, and says that eyeballs can reach that route using the corresponding
|
||||||
|
// tunnel.
|
||||||
|
type Route struct {
|
||||||
|
Network net.IPNet
|
||||||
|
TunnelID uuid.UUID
|
||||||
|
Comment string
|
||||||
|
CreatedAt time.Time
|
||||||
|
DeletedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableString outputs a table row summarizing the route, to be used
|
||||||
|
// when showing the user their routing table.
|
||||||
|
func (r Route) TableString() string {
|
||||||
|
deletedColumn := "-"
|
||||||
|
if !r.DeletedAt.IsZero() {
|
||||||
|
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\t",
|
||||||
|
r.Network.String(),
|
||||||
|
r.Comment,
|
||||||
|
r.TunnelID,
|
||||||
|
r.CreatedAt.Format(time.RFC3339),
|
||||||
|
deletedColumn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||||
|
func (r *Route) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
|
// This is the raw JSON format that cloudflared receives from tunnelstore.
|
||||||
|
// Note it does not understand types like IPNet.
|
||||||
|
var resp struct {
|
||||||
|
Network string `json:"network"`
|
||||||
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt time.Time `json:"deleted_at"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the raw JSON into a properly-typed response.
|
||||||
|
_, network, err := net.ParseCIDR(resp.Network)
|
||||||
|
if err != nil || network == nil {
|
||||||
|
return fmt.Errorf("backend returned invalid network %s", resp.Network)
|
||||||
|
}
|
||||||
|
r.Network = *network
|
||||||
|
r.TunnelID = resp.TunnelID
|
||||||
|
r.Comment = resp.Comment
|
||||||
|
r.CreatedAt = resp.CreatedAt
|
||||||
|
r.DeletedAt = resp.DeletedAt
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoute has all the parameters necessary to add a new route to the table.
|
||||||
|
type NewRoute struct {
|
||||||
|
Network net.IPNet
|
||||||
|
TunnelID uuid.UUID
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||||
|
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
Network string `json:"network"`
|
||||||
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}{
|
||||||
|
TunnelID: r.TunnelID,
|
||||||
|
Comment: r.Comment,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package teamnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalRoute(t *testing.T) {
|
||||||
|
// Response from the teamnet route backend
|
||||||
|
data := `{
|
||||||
|
"network":"10.1.2.40/29",
|
||||||
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
|
"comment":"test",
|
||||||
|
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||||
|
"deleted_at":null
|
||||||
|
}`
|
||||||
|
var r Route
|
||||||
|
err := r.UnmarshalJSON([]byte(data))
|
||||||
|
|
||||||
|
// Check everything worked
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||||
|
require.Equal(t, "test", r.Comment)
|
||||||
|
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, *cidr, r.Network)
|
||||||
|
require.Equal(t, "test", r.Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalNewRoute(t *testing.T) {
|
||||||
|
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, network)
|
||||||
|
newRoute := NewRoute{
|
||||||
|
Network: *network,
|
||||||
|
TunnelID: uuid.New(),
|
||||||
|
Comment: "hi",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test where receiver is struct
|
||||||
|
serialized, err := json.Marshal(newRoute)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||||
|
|
||||||
|
// Test where receiver is pointer to struct
|
||||||
|
serialized, err = json.Marshal(&newRoute)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteTableString(t *testing.T) {
|
||||||
|
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, network)
|
||||||
|
r := Route{
|
||||||
|
Network: *network,
|
||||||
|
}
|
||||||
|
row := r.TableString()
|
||||||
|
fmt.Println(row)
|
||||||
|
require.True(t, strings.HasPrefix(row, "1.2.3.4/32"))
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package teamnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
filterDeleted = cli.BoolFlag{
|
||||||
|
Name: "filter-is-deleted",
|
||||||
|
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||||
|
}
|
||||||
|
filterTunnelID = cli.StringFlag{
|
||||||
|
Name: "filter-tunnel-id",
|
||||||
|
Usage: "Show only routes with the given tunnel ID.",
|
||||||
|
}
|
||||||
|
filterSubset = 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{
|
||||||
|
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{
|
||||||
|
Name: "filter-comment-is",
|
||||||
|
Usage: "Show only routes with this comment.",
|
||||||
|
}
|
||||||
|
// Flags contains all filter flags.
|
||||||
|
Flags = []cli.Flag{
|
||||||
|
&filterDeleted,
|
||||||
|
&filterTunnelID,
|
||||||
|
&filterSubset,
|
||||||
|
&filterSuperset,
|
||||||
|
&filterComment,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter which routes get queried.
|
||||||
|
type Filter struct {
|
||||||
|
queryParams url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromCLI parses CLI flags to discover which filters should get applied.
|
||||||
|
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||||
|
f := &Filter{
|
||||||
|
queryParams: url.Values{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set deletion filter
|
||||||
|
if flag := filterDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||||
|
f.deleted()
|
||||||
|
} else {
|
||||||
|
f.notDeleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
if subset, err := cidrFromFlag(c, filterSubset); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if subset != nil {
|
||||||
|
f.networkIsSupersetOf(*subset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if superset, err := cidrFromFlag(c, filterSuperset); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if superset != nil {
|
||||||
|
f.networkIsSupersetOf(*superset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment := c.String(filterComment.Name); comment != "" {
|
||||||
|
f.commentIs(comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
||||||
|
u, err := uuid.Parse(tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Couldn't parse UUID from --filter-tunnel-id")
|
||||||
|
}
|
||||||
|
f.tunnelID(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses a CIDR from the flag. If the flag was unset, returns (nil, nil).
|
||||||
|
func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||||
|
if !c.IsSet(flag.Name) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, subset, err := net.ParseCIDR(c.String(flag.Name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if subset == nil {
|
||||||
|
return nil, fmt.Errorf("Invalid CIDR supplied for %s", flag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) commentIs(comment string) {
|
||||||
|
f.queryParams.Set("comment", comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) notDeleted() {
|
||||||
|
f.queryParams.Set("is_deleted", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) deleted() {
|
||||||
|
f.queryParams.Set("is_deleted", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) networkIsSubsetOf(superset net.IPNet) {
|
||||||
|
f.queryParams.Set("network_subset", superset.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) networkIsSupersetOf(subset net.IPNet) {
|
||||||
|
f.queryParams.Set("network_superset", subset.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) existedAt(existedAt time.Time) {
|
||||||
|
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) tunnelID(id uuid.UUID) {
|
||||||
|
f.queryParams.Set("tunnel_id", id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Filter) Encode() string {
|
||||||
|
return f.queryParams.Encode()
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -185,12 +186,17 @@ func (res *LBRouteResult) SuccessSummary() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
|
// Named Tunnels endpoints
|
||||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||||
DeleteTunnel(tunnelID uuid.UUID) error
|
DeleteTunnel(tunnelID uuid.UUID) error
|
||||||
ListTunnels(filter *Filter) ([]*Tunnel, error)
|
ListTunnels(filter *Filter) ([]*Tunnel, error)
|
||||||
CleanupConnections(tunnelID uuid.UUID) error
|
CleanupConnections(tunnelID uuid.UUID) error
|
||||||
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
|
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
|
||||||
|
|
||||||
|
// Teamnet endpoints
|
||||||
|
ListRoutes(filter *teamnet.Filter) ([]*teamnet.Route, error)
|
||||||
|
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RESTClient struct {
|
type RESTClient struct {
|
||||||
|
@ -204,6 +210,7 @@ type RESTClient struct {
|
||||||
type baseEndpoints struct {
|
type baseEndpoints struct {
|
||||||
accountLevel url.URL
|
accountLevel url.URL
|
||||||
zoneLevel url.URL
|
zoneLevel url.URL
|
||||||
|
accountRoutes url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Client = (*RESTClient)(nil)
|
var _ Client = (*RESTClient)(nil)
|
||||||
|
@ -216,6 +223,10 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||||
}
|
}
|
||||||
|
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/routes", baseURL, accountTag))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||||
|
}
|
||||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||||
|
@ -224,6 +235,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
|
||||||
baseEndpoints: &baseEndpoints{
|
baseEndpoints: &baseEndpoints{
|
||||||
accountLevel: *accountLevelEndpoint,
|
accountLevel: *accountLevelEndpoint,
|
||||||
zoneLevel: *zoneLevelEndpoint,
|
zoneLevel: *zoneLevelEndpoint,
|
||||||
|
accountRoutes: *accountRoutesEndpoint,
|
||||||
},
|
},
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
|
@ -388,7 +400,10 @@ func parseResponse(reader io.Reader, data interface{}) error {
|
||||||
}
|
}
|
||||||
// At this point we know the API call succeeded, so, parse out the inner
|
// At this point we know the API call succeeded, so, parse out the inner
|
||||||
// result into the datatype provided as a parameter.
|
// result into the datatype provided as a parameter.
|
||||||
return json.Unmarshal(result.Result, &data)
|
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) {
|
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package tunnelstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RESTClient) ListRoutes(filter *teamnet.Filter) ([]*teamnet.Route, 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 parseListRoutes(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("list routes", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseListRoutes(body io.ReadCloser) ([]*teamnet.Route, error) {
|
||||||
|
var routes []*teamnet.Route
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in New Issue