TUN-5361: Commands for managing virtual networks
This commit is contained in:
parent
6cc7d99e32
commit
eec6b87eea
|
@ -102,6 +102,8 @@ func Commands() []*cli.Command {
|
||||||
buildLoginSubcommand(false),
|
buildLoginSubcommand(false),
|
||||||
buildCreateCommand(),
|
buildCreateCommand(),
|
||||||
buildRouteCommand(),
|
buildRouteCommand(),
|
||||||
|
// TODO TUN-5477 this should not be hidden
|
||||||
|
buildVirtualNetworkSubcommand(true),
|
||||||
buildRunCommand(),
|
buildRunCommand(),
|
||||||
buildListCommand(),
|
buildListCommand(),
|
||||||
buildInfoCommand(),
|
buildInfoCommand(),
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sc *subcommandContext) addVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||||
|
}
|
||||||
|
return client.CreateVirtualNetwork(newVnet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *subcommandContext) listVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, noClientMsg)
|
||||||
|
}
|
||||||
|
return client.ListVirtualNetworks(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, noClientMsg)
|
||||||
|
}
|
||||||
|
return client.DeleteVirtualNetwork(vnetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
||||||
|
client, err := sc.client()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, noClientMsg)
|
||||||
|
}
|
||||||
|
return client.UpdateVirtualNetwork(vnetId, updates)
|
||||||
|
}
|
|
@ -329,7 +329,7 @@ func listCommand(c *cli.Context) error {
|
||||||
if len(tunnels) > 0 {
|
if len(tunnels) > 0 {
|
||||||
formatAndPrintTunnelList(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("No tunnels were found for the given filter flags. You can use 'cloudflared tunnel create' to create a tunnel.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -98,7 +98,7 @@ func showRoutesCommand(c *cli.Context) error {
|
||||||
if len(routes) > 0 {
|
if len(routes) > 0 {
|
||||||
formatAndPrintRouteList(routes)
|
formatAndPrintRouteList(routes)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("You have no routes, use 'cloudflared tunnel route ip add' to add a route")
|
fmt.Println("No routes were found for the given filter flags. You can use 'cloudflared tunnel route ip add' to add a route.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||||
|
"github.com/cloudflare/cloudflared/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
makeDefaultFlag = &cli.BoolFlag{
|
||||||
|
Name: "default",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
Usage: "The virtual network becomes the default one for the account. This means that all operations that " +
|
||||||
|
"omit a virtual network will now implicitly be using this virtual network (i.e., the default one) such " +
|
||||||
|
"as new IP routes that are created. When this flag is not set, the virtual network will not become the " +
|
||||||
|
"default one in the account.",
|
||||||
|
}
|
||||||
|
newNameFlag = &cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Usage: "The new name for the virtual network.",
|
||||||
|
}
|
||||||
|
newCommentFlag = &cli.StringFlag{
|
||||||
|
Name: "comment",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "A new comment describing the purpose of the virtual network.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildVirtualNetworkSubcommand(hidden bool) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "network",
|
||||||
|
Usage: "Configure and query virtual networks to manage private IP routes with overlapping IPs.",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] network COMMAND [arguments...]",
|
||||||
|
Description: `cloudflared allows to manage IP routes that expose origins in your private network space via their IP directly
|
||||||
|
to clients outside (e.g. using WARP client) --- those are configurable via "cloudflared tunnel route ip" commands.
|
||||||
|
By default, all those IP routes live in the same virtual network. Managing virtual networks (e.g. by creating a
|
||||||
|
new one) becomes relevant when you have different private networks that have overlapping IPs. E.g.: if you have
|
||||||
|
a private network A running Tunnel 1, and private network B running Tunnel 2, it is possible that both Tunnels
|
||||||
|
expose the same IP space (say 10.0.0.0/8); to handle that, you have to add each IP Route (one that points to
|
||||||
|
Tunnel 1 and another that points to Tunnel 2) in different Virtual Networks. That way, if your clients are on
|
||||||
|
Virtual Network X, they will see Tunnel 1 (via Route A) and not see Tunnel 2 (since its Route B is associated
|
||||||
|
to another Virtual Network Y).`,
|
||||||
|
Hidden: hidden,
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
Action: cliutil.ConfiguredAction(addVirtualNetworkCommand),
|
||||||
|
Usage: "Add a new virtual network to which IP routes can be attached",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] network add [flags] NAME [\"comment\"]",
|
||||||
|
Description: `Adds a new virtual network. You can then attach IP routes to this virtual network with "cloudflared tunnel route ip"
|
||||||
|
commands. By doing so, such route(s) become segregated from route(s) in another virtual networks. Note that all
|
||||||
|
routes exist within some virtual network. If you do not specify any, then the system pre-creates a default virtual
|
||||||
|
network to which all routes belong. That is fine if you do not have overlapping IPs within different physical
|
||||||
|
private networks in your infrastructure exposed via Cloudflare Tunnel. Note: if a virtual network is added as
|
||||||
|
the new default, then the previous existing default virtual network will be automatically modified to no longer
|
||||||
|
be the current default.`,
|
||||||
|
Flags: []cli.Flag{makeDefaultFlag},
|
||||||
|
Hidden: hidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Action: cliutil.ConfiguredAction(listVirtualNetworksCommand),
|
||||||
|
Usage: "Lists the virtual networks",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] network list [flags]",
|
||||||
|
Description: "Lists the virtual networks based on the given filter flags.",
|
||||||
|
Flags: listVirtualNetworksFlags(),
|
||||||
|
Hidden: hidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Action: cliutil.ConfiguredAction(deleteVirtualNetworkCommand),
|
||||||
|
Usage: "Delete a virtual network",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] network delete VIRTUAL_NETWORK",
|
||||||
|
Description: `Deletes the virtual network (given its ID or name). This is only possible if that virtual network is unused.
|
||||||
|
A virtual network may be used by IP routes or by WARP devices.`,
|
||||||
|
Hidden: hidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "update",
|
||||||
|
Action: cliutil.ConfiguredAction(updateVirtualNetworkCommand),
|
||||||
|
Usage: "Update a virtual network",
|
||||||
|
UsageText: "cloudflared tunnel [--config FILEPATH] network update [flags] VIRTUAL_NETWORK",
|
||||||
|
Description: `Updates the virtual network (given its ID or name). If this virtual network is updated to become the new
|
||||||
|
default, then the previously existing default virtual network will also be modified to no longer be the default.
|
||||||
|
You cannot update a virtual network to not be the default anymore directly. Instead, you should create a new
|
||||||
|
default or update an existing one to become the default.`,
|
||||||
|
Flags: []cli.Flag{newNameFlag, newCommentFlag, makeDefaultFlag},
|
||||||
|
Hidden: hidden,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listVirtualNetworksFlags() []cli.Flag {
|
||||||
|
flags := make([]cli.Flag, 0)
|
||||||
|
flags = append(flags, vnet.FilterFlags...)
|
||||||
|
flags = append(flags, outputFormatFlag)
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func addVirtualNetworkCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.NArg() < 1 {
|
||||||
|
return errors.New("You must supply at least 1 argument, the name of the virtual network you wish to add.")
|
||||||
|
}
|
||||||
|
|
||||||
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
|
args := c.Args()
|
||||||
|
|
||||||
|
name := args.Get(0)
|
||||||
|
|
||||||
|
comment := ""
|
||||||
|
if c.NArg() >= 2 {
|
||||||
|
comment = args.Get(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
newVnet := vnet.NewVirtualNetwork{
|
||||||
|
Name: name,
|
||||||
|
Comment: comment,
|
||||||
|
IsDefault: c.Bool(makeDefaultFlag.Name),
|
||||||
|
}
|
||||||
|
createdVnet, err := sc.addVirtualNetwork(newVnet)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Could not add virtual network")
|
||||||
|
}
|
||||||
|
|
||||||
|
extraMsg := ""
|
||||||
|
if createdVnet.IsDefault {
|
||||||
|
extraMsg = " (as the new default for this account) "
|
||||||
|
}
|
||||||
|
fmt.Printf(
|
||||||
|
"Successfully added virtual 'network' %s with ID: %s%s\n"+
|
||||||
|
"You can now add IP routes attached to this virtual network. See `cloudflared tunnel route ip add -help`\n",
|
||||||
|
name, createdVnet.ID, extraMsg,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listVirtualNetworksCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
warningChecker := updater.StartWarningCheck(c)
|
||||||
|
defer warningChecker.LogWarningIfAny(sc.log)
|
||||||
|
|
||||||
|
filter, err := vnet.NewFromCLI(c)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid flags for filtering virtual networks")
|
||||||
|
}
|
||||||
|
|
||||||
|
vnets, err := sc.listVirtualNetworks(filter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||||
|
return renderOutput(outputFormat, vnets)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vnets) > 0 {
|
||||||
|
formatAndPrintVnetsList(vnets)
|
||||||
|
} else {
|
||||||
|
fmt.Println("No virtual networks were found for the given filter flags. You can use 'cloudflared tunnel network add' to add a virtual network.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteVirtualNetworkCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.NArg() != 1 {
|
||||||
|
return errors.New("You must supply exactly one argument, either the ID or name of the virtual network to delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
input := c.Args().Get(0)
|
||||||
|
vnetId, err := getVnetId(sc, input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.deleteVirtualNetwork(vnetId); err != nil {
|
||||||
|
return errors.Wrap(err, "API error")
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully deleted virtual network '%s'\n", input)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVirtualNetworkCommand(c *cli.Context) error {
|
||||||
|
sc, err := newSubcommandContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.NArg() != 1 {
|
||||||
|
return errors.New(" You must supply exactly one argument, either the ID or (current) name of the virtual network to update")
|
||||||
|
}
|
||||||
|
|
||||||
|
input := c.Args().Get(0)
|
||||||
|
vnetId, err := getVnetId(sc, input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := vnet.UpdateVirtualNetwork{}
|
||||||
|
|
||||||
|
if c.IsSet(newNameFlag.Name) {
|
||||||
|
newName := c.String(newNameFlag.Name)
|
||||||
|
updates.Name = &newName
|
||||||
|
}
|
||||||
|
if c.IsSet(newCommentFlag.Name) {
|
||||||
|
newComment := c.String(newCommentFlag.Name)
|
||||||
|
updates.Comment = &newComment
|
||||||
|
}
|
||||||
|
if c.IsSet(makeDefaultFlag.Name) {
|
||||||
|
isDefault := c.Bool(makeDefaultFlag.Name)
|
||||||
|
updates.IsDefault = &isDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.updateVirtualNetwork(vnetId, updates); err != nil {
|
||||||
|
return errors.Wrap(err, "API error")
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully updated virtual network '%s'\n", input)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||||
|
val, err := uuid.Parse(input)
|
||||||
|
if err == nil {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := vnet.NewFilter()
|
||||||
|
filter.WithDeleted(false)
|
||||||
|
filter.ByName(input)
|
||||||
|
|
||||||
|
vnets, err := sc.listVirtualNetworks(filter)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vnets) != 1 {
|
||||||
|
return uuid.Nil, fmt.Errorf("there should only be 1 non-deleted virtual network named %s", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vnets[0].ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) {
|
||||||
|
const (
|
||||||
|
minWidth = 0
|
||||||
|
tabWidth = 8
|
||||||
|
padding = 1
|
||||||
|
padChar = ' '
|
||||||
|
flags = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(writer, "ID\tNAME\tIS DEFAULT\tCOMMENT\tCREATED\tDELETED\t")
|
||||||
|
|
||||||
|
for _, virtualNetwork := range vnets {
|
||||||
|
formattedStr := virtualNetwork.TableString()
|
||||||
|
_, _ = fmt.Fprintln(writer, formattedStr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -86,6 +87,10 @@ func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||||
f.tunnelID(u)
|
f.tunnelID(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||||
|
f.MaxFetchSize(uint(maxFetch))
|
||||||
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +138,10 @@ func (f *Filter) tunnelID(id uuid.UUID) {
|
||||||
f.queryParams.Set("tunnel_id", id.String())
|
f.queryParams.Set("tunnel_id", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Filter) MaxFetchSize(max uint) {
|
||||||
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
|
}
|
||||||
|
|
||||||
func (f Filter) Encode() string {
|
func (f Filter) Encode() string {
|
||||||
return f.queryParams.Encode()
|
return f.queryParams.Encode()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,10 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
|
"github.com/cloudflare/cloudflared/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -238,6 +238,12 @@ type Client interface {
|
||||||
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
||||||
DeleteRoute(network net.IPNet) error
|
DeleteRoute(network net.IPNet) error
|
||||||
GetByIP(ip net.IP) (teamnet.DetailedRoute, error)
|
GetByIP(ip net.IP) (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 {
|
type RESTClient struct {
|
||||||
|
@ -252,6 +258,7 @@ type baseEndpoints struct {
|
||||||
accountLevel url.URL
|
accountLevel url.URL
|
||||||
zoneLevel url.URL
|
zoneLevel url.URL
|
||||||
accountRoutes url.URL
|
accountRoutes url.URL
|
||||||
|
accountVnets url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Client = (*RESTClient)(nil)
|
var _ Client = (*RESTClient)(nil)
|
||||||
|
@ -268,6 +275,10 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||||
}
|
}
|
||||||
|
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/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))
|
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")
|
||||||
|
@ -282,6 +293,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
|
||||||
accountLevel: *accountLevelEndpoint,
|
accountLevel: *accountLevelEndpoint,
|
||||||
zoneLevel: *zoneLevelEndpoint,
|
zoneLevel: *zoneLevelEndpoint,
|
||||||
accountRoutes: *accountRoutesEndpoint,
|
accountRoutes: *accountRoutesEndpoint,
|
||||||
|
accountVnets: *accountVnetsEndpoint,
|
||||||
},
|
},
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
|
|
|
@ -81,12 +81,6 @@ func (r *RESTClient) GetByIP(ip net.IP) (teamnet.DetailedRoute, error) {
|
||||||
return teamnet.DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
return teamnet.DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseListRoutes(body io.ReadCloser) ([]*teamnet.Route, error) {
|
|
||||||
var routes []*teamnet.Route
|
|
||||||
err := parseResponse(body, &routes)
|
|
||||||
return routes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*teamnet.DetailedRoute, error) {
|
func parseListDetailedRoutes(body io.ReadCloser) ([]*teamnet.DetailedRoute, error) {
|
||||||
var routes []*teamnet.DetailedRoute
|
var routes []*teamnet.DetailedRoute
|
||||||
err := parseResponse(body, &routes)
|
err := parseResponse(body, &routes)
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package tunnelstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/vnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
||||||
|
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
||||||
|
if err != nil {
|
||||||
|
return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return parseVnet(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vnet.VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
||||||
|
endpoint := r.baseEndpoints.accountVnets
|
||||||
|
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 parseListVnets(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, r.statusCodeToError("list virtual networks", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||||
|
endpoint := r.baseEndpoints.accountVnets
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||||
|
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 := parseVnet(resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.statusCodeToError("delete virtual network", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
||||||
|
endpoint := r.baseEndpoints.accountVnets
|
||||||
|
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||||
|
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
_, err := parseVnet(resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.statusCodeToError("update virtual network", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) {
|
||||||
|
var vnets []*vnet.VirtualNetwork
|
||||||
|
err := parseResponse(body, &vnets)
|
||||||
|
return vnets, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) {
|
||||||
|
var vnet vnet.VirtualNetwork
|
||||||
|
err := parseResponse(body, &vnet)
|
||||||
|
return vnet, err
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package vnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVirtualNetworkJsonRoundtrip(t *testing.T) {
|
||||||
|
data := `{
|
||||||
|
"id":"74fce949-351b-4752-b261-81a56cfd3130",
|
||||||
|
"comment":"New York DC1",
|
||||||
|
"name":"us-east-1",
|
||||||
|
"is_default_network":true,
|
||||||
|
"created_at":"2021-11-26T14:40:02.600673Z",
|
||||||
|
"deleted_at":"2021-12-01T10:23:13.102645Z"
|
||||||
|
}`
|
||||||
|
var v VirtualNetwork
|
||||||
|
err := json.Unmarshal([]byte(data), &v)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uuid.MustParse("74fce949-351b-4752-b261-81a56cfd3130"), v.ID)
|
||||||
|
require.Equal(t, "us-east-1", v.Name)
|
||||||
|
require.Equal(t, "New York DC1", v.Comment)
|
||||||
|
require.Equal(t, true, v.IsDefault)
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
obtainedJson := string(bytes)
|
||||||
|
data = strings.Replace(data, "\t", "", -1)
|
||||||
|
data = strings.Replace(data, "\n", "", -1)
|
||||||
|
require.Equal(t, data, obtainedJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalNewVnet(t *testing.T) {
|
||||||
|
newVnet := NewVirtualNetwork{
|
||||||
|
Name: "eu-west-1",
|
||||||
|
Comment: "London office",
|
||||||
|
IsDefault: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized, err := json.Marshal(newVnet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, strings.Contains(string(serialized), newVnet.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUpdateVnet(t *testing.T) {
|
||||||
|
newName := "bulgaria-1"
|
||||||
|
updates := UpdateVirtualNetwork{
|
||||||
|
Name: &newName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test where receiver is struct
|
||||||
|
serialized, err := json.Marshal(updates)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, strings.Contains(string(serialized), newName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVnetTableString(t *testing.T) {
|
||||||
|
virtualNet := VirtualNetwork{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Name: "us-east-1",
|
||||||
|
Comment: "New York DC1",
|
||||||
|
IsDefault: true,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
DeletedAt: time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
row := virtualNet.TableString()
|
||||||
|
require.True(t, strings.HasPrefix(row, virtualNet.ID.String()))
|
||||||
|
require.True(t, strings.Contains(row, virtualNet.Name))
|
||||||
|
require.True(t, strings.Contains(row, virtualNet.Comment))
|
||||||
|
require.True(t, strings.Contains(row, "true"))
|
||||||
|
require.True(t, strings.HasSuffix(row, "-\t"))
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package vnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
filterId = cli.StringFlag{
|
||||||
|
Name: "id",
|
||||||
|
Usage: "List virtual networks with the given `ID`",
|
||||||
|
}
|
||||||
|
filterName = cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Usage: "List virtual networks with the given `NAME`",
|
||||||
|
}
|
||||||
|
filterDefault = 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{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter which virtual networks get queried.
|
||||||
|
type Filter struct {
|
||||||
|
queryParams url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilter() *Filter {
|
||||||
|
return &Filter{
|
||||||
|
queryParams: url.Values{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) ById(vnetId uuid.UUID) {
|
||||||
|
f.queryParams.Set("id", vnetId.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) ByName(name string) {
|
||||||
|
f.queryParams.Set("name", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) ByDefaultStatus(isDefault bool) {
|
||||||
|
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) WithDeleted(isDeleted bool) {
|
||||||
|
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filter) MaxFetchSize(max uint) {
|
||||||
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Filter) 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()
|
||||||
|
|
||||||
|
if id := c.String("id"); id != "" {
|
||||||
|
vnetId, err := uuid.Parse(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%s is not a valid virtual network ID", id)
|
||||||
|
}
|
||||||
|
f.ById(vnetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name := c.String("name"); name != "" {
|
||||||
|
f.ByName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("is-default") {
|
||||||
|
f.ByDefaultStatus(c.Bool("is-default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WithDeleted(c.Bool("show-deleted"))
|
||||||
|
|
||||||
|
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||||
|
f.MaxFetchSize(uint(maxFetch))
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
Loading…
Reference in New Issue