TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service

This commit is contained in:
Igor Postelnik
2020-05-21 15:36:49 -05:00
parent 7a77ead423
commit a908453aa4
5 changed files with 412 additions and 14 deletions

View File

@@ -1,12 +1,35 @@
package cliutil
import "gopkg.in/urfave/cli.v2"
import (
"fmt"
"gopkg.in/urfave/cli.v2"
)
type usageError string
func (ue usageError) Error() string {
return string(ue)
}
func UsageError(format string, args ...interface{}) error {
if len(args) == 0 {
return usageError(format)
} else {
msg := fmt.Sprintf(format, args...)
return usageError(msg)
}
}
// Ensures exit with error code if actionFunc returns an error
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error {
err := actionFunc(ctx)
if err != nil {
if _, ok := err.(usageError); ok {
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
return cli.Exit(msg, -1)
}
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
cli.HandleExitCoder(err)
err = cli.Exit(err.Error(), 1)
@@ -14,4 +37,3 @@ func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return err
}
}

View File

@@ -168,6 +168,9 @@ func Commands() []*cli.Command {
c.Hidden = false
subcommands = append(subcommands, &c)
}
subcommands = append(subcommands, buildCreateCommand())
subcommands = append(subcommands, buildListCommand())
subcommands = append(subcommands, buildDeleteCommand())
cmds = append(cmds, &cli.Command{
Name: "tunnel",
@@ -175,7 +178,7 @@ func Commands() []*cli.Command {
Before: Before,
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
ArgsUsage: "[origin-url]",
ArgsUsage: " ",
Description: `Argo Tunnel asks you to specify a hostname on a Cloudflare-powered
domain you control and a local address. Traffic from that hostname is routed
(optionally via a Cloudflare Load Balancer) to this machine and appears on the
@@ -843,6 +846,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_API_CA_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-url",
Usage: "Base URL for Cloudflare API v4",
EnvVars: []string{"TUNNEL_API_URL"},
Value: "https://api.cloudflare.com/client/v4",
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics",
Value: "localhost:",

View File

@@ -102,27 +102,29 @@ func dnsProxyStandAlone(c *cli.Context) bool {
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
}
func getOriginCert(c *cli.Context) ([]byte, error) {
if c.String("origincert") == "" {
func findOriginCert(c *cli.Context) (string, error) {
originCertPath := c.String("origincert")
if originCertPath == "" {
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
if isRunningFromTerminal() {
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
} else {
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
return nil, fmt.Errorf("Client didn't specify origincert path")
return "", fmt.Errorf("Client didn't specify origincert path")
}
}
// Check that the user has acquired a certificate using the login command
originCertPath, err := homedir.Expand(c.String("origincert"))
var err error
originCertPath, err = homedir.Expand(originCertPath)
if err != nil {
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
logger.WithError(err).Errorf("Cannot resolve path %s", originCertPath)
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
}
// Check that the user has acquired a certificate using the login command
ok, err := config.FileExists(originCertPath)
if err != nil {
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
logger.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
}
if !ok {
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
@@ -134,8 +136,15 @@ If you don't have a certificate signed by Cloudflare, run the command:
%s login
`, originCertPath, os.Args[0])
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
return "", fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
}
return originCertPath, nil
}
func readOriginCert(originCertPath string) ([]byte, error) {
logger.Debugf("Reading origin cert from %s", originCertPath)
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert, err := ioutil.ReadFile(originCertPath)
if err != nil {
@@ -145,6 +154,14 @@ If you don't have a certificate signed by Cloudflare, run the command:
return originCert, nil
}
func getOriginCert(c *cli.Context) ([]byte, error) {
if originCertPath, err := findOriginCert(c); err != nil {
return nil, err
} else {
return readOriginCert(originCertPath)
}
}
func prepareTunnelConfig(
c *cli.Context,
buildInfo *buildinfo.BuildInfo,

View File

@@ -0,0 +1,165 @@
package tunnel
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/tunnelstore"
)
var (
outputFormatFlag = &cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
}
)
const hideSubcommands = true
func buildCreateCommand() *cli.Command {
return &cli.Command{
Name: "create",
Action: cliutil.ErrorHandler(createTunnel),
Usage: "Create a new tunnel with given name",
ArgsUsage: "TUNNEL-NAME",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
}
}
func createTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`)
}
name := c.Args().First()
client, err := newTunnelstoreClient(c)
if err != nil {
return err
}
tunnel, err := client.CreateTunnel(name)
if err != nil {
return errors.Wrap(err, "Error creating a new tunnel")
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, &tunnel)
}
logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return nil
}
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
}
}
func listTunnels(c *cli.Context) error {
client, err := newTunnelstoreClient(c)
if err != nil {
return err
}
tunnels, err := client.ListTunnels()
if err != nil {
return errors.Wrap(err, "Error listing tunnels")
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, tunnels)
}
if len(tunnels) > 0 {
const listFormat = "%-40s%-40s%s\n"
fmt.Printf(listFormat, "ID", "NAME", "CREATED")
for _, t := range tunnels {
fmt.Printf(listFormat, t.ID, t.Name, t.CreatedAt.Format(time.RFC3339))
}
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
}
return nil
}
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteTunnel),
Usage: "Delete existing tunnel with given ID",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
}
}
func deleteTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
}
id := c.Args().First()
client, err := newTunnelstoreClient(c)
if err != nil {
return err
}
if err := client.DeleteTunnel(id); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", id)
}
return nil
}
func renderOutput(format string, v interface{}) error {
switch format {
case "json":
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(v)
case "yaml":
return yaml.NewEncoder(os.Stdout).Encode(v)
default:
return errors.Errorf("Unknown output format '%s'", format)
}
}
func newTunnelstoreClient(c *cli.Context) (tunnelstore.Client, error) {
originCertPath, err := findOriginCert(c)
if err != nil {
return nil, errors.Wrap(err, "Error locating origin cert")
}
blocks, err := readOriginCert(originCertPath)
if err != nil {
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err := certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
client := tunnelstore.NewRESTClient(c.String("api-url"), cert.AccountID, cert.ServiceKey)
return client, nil
}