TUN-3463: Let users run a named tunnel via config file setting

This commit is contained in:
Igor Postelnik 2020-10-15 15:08:57 -05:00
parent acd03e36e6
commit 051908aaef
5 changed files with 104 additions and 50 deletions

View File

@ -10,9 +10,11 @@ import (
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/validation"
) )
@ -199,7 +201,7 @@ func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
func ReadRules(c *cli.Context) (ingress.Ingress, error) { func ReadRules(c *cli.Context) (ingress.Ingress, error) {
configFilePath := c.String("config") configFilePath := c.String("config")
if configFilePath == "" { if configFilePath == "" {
return ingress.Ingress{}, ErrNoConfigFile return ingress.Ingress{}, ingress.ErrNoIngressRules
} }
fmt.Printf("Reading from config file %s\n", configFilePath) fmt.Printf("Reading from config file %s\n", configFilePath)
configBytes, err := ioutil.ReadFile(configFilePath) configBytes, err := ioutil.ReadFile(configFilePath)
@ -209,3 +211,31 @@ func ReadRules(c *cli.Context) (ingress.Ingress, error) {
rules, err := ingress.ParseIngress(configBytes) rules, err := ingress.ParseIngress(configBytes)
return rules, err return rules, err
} }
var configFileInputSource struct {
lastLoadedFile string
context altsrc.InputSourceContext
}
// GetConfigFileSource returns InputSourceContext initialized from the configuration file.
// On repeat calls returns with the same file, returns without reading the file again; however,
// if value of "config" flag changes, will read the new config file
func GetConfigFileSource(c *cli.Context, log logger.Service) (altsrc.InputSourceContext, error) {
configFile := c.String("config")
if configFileInputSource.lastLoadedFile == configFile {
if configFileInputSource.context == nil {
return nil, ErrNoConfigFile
}
return configFileInputSource.context, nil
}
configFileInputSource.lastLoadedFile = configFile
log.Debugf("Loading configuration from %s", configFile)
src, err := altsrc.NewYamlSourceFromFile(configFile)
if err != nil {
return nil, err
}
configFileInputSource.context = src
return src, nil
}

View File

@ -199,24 +199,29 @@ func buildIngressSubcommand() *cli.Command {
} }
func TunnelCommand(c *cli.Context) error { func TunnelCommand(c *cli.Context) error {
if name := c.String("name"); name != "" { // Start a named tunnel sc, err := newSubcommandContext(c)
return adhocNamedTunnel(c, name) if err != nil {
} else { // Start a classic tunnel return err
return classicTunnel(c)
} }
if name := c.String("name"); name != "" { // Start a named tunnel
return runAdhocNamedTunnel(sc, name)
}
if ref, err := sc.getConfigFileTunnelRef(); err != nil {
return err
} else if ref != "" {
return runNamedTunnel(sc, ref)
}
// Start a classic tunnel
return runClassicTunnel(sc)
} }
func Init(v string, s, g chan struct{}) { func Init(v string, s, g chan struct{}) {
version, shutdownC, graceShutdownC = v, s, g version, shutdownC, graceShutdownC = v, s, g
} }
// adhocNamedTunnel create, route and run a named tunnel in one command // runAdhocNamedTunnel create, route and run a named tunnel in one command
func adhocNamedTunnel(c *cli.Context, name string) error { func runAdhocNamedTunnel(sc *subcommandContext, name string) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
tunnel, ok, err := sc.tunnelActive(name) tunnel, ok, err := sc.tunnelActive(name)
if err != nil || !ok { if err != nil || !ok {
tunnel, err = sc.create(name) tunnel, err = sc.create(name)
@ -227,7 +232,7 @@ func adhocNamedTunnel(c *cli.Context, name string) error {
sc.logger.Infof("Tunnel already created with ID %s", tunnel.ID) sc.logger.Infof("Tunnel already created with ID %s", tunnel.ID)
} }
if r, ok := routeFromFlag(c); ok { if r, ok := routeFromFlag(sc.c); ok {
if res, err := sc.route(tunnel.ID, r); err != nil { if res, err := sc.route(tunnel.ID, r); err != nil {
sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err) sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err)
} else { } else {
@ -242,14 +247,9 @@ func adhocNamedTunnel(c *cli.Context, name string) error {
return nil return nil
} }
// classicTunnel creates a "classic" non-named tunnel // runClassicTunnel creates a "classic" non-named tunnel
func classicTunnel(c *cli.Context) error { func runClassicTunnel(sc *subcommandContext) error {
sc, err := newSubcommandContext(c) return StartServer(sc.c, version, shutdownC, graceShutdownC, nil, sc.logger, sc.isUIEnabled)
if err != nil {
return err
}
return StartServer(c, version, shutdownC, graceShutdownC, nil, sc.logger, sc.isUIEnabled)
} }
func routeFromFlag(c *cli.Context) (tunnelstore.Route, bool) { func routeFromFlag(c *cli.Context) (tunnelstore.Route, bool) {
@ -571,33 +571,25 @@ func forceSetFlag(c *cli.Context, name, value string) {
} }
func SetFlagsFromConfigFile(c *cli.Context) error { func SetFlagsFromConfigFile(c *cli.Context) error {
logger, err := createLogger(c, false, false) log, err := createLogger(c, false, false)
if err != nil { if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err) return cliutil.PrintLoggerSetupError("error setting up logger", err)
} }
configFile := c.String("config") inputSource, err := config.GetConfigFileSource(c, log)
if configFile == "" {
logger.Debugf(config.ErrNoConfigFile.Error())
return nil
}
inputSource, err := altsrc.NewYamlSourceFromFile(configFile)
if err != nil { if err != nil {
logger.Errorf("Cannot load configuration from %s: %s", configFile, err) if err == config.ErrNoConfigFile {
return nil
}
return err return err
} }
if inputSource != nil { targetFlags := c.Command.Flags
targetFlags := c.Command.Flags if c.Command.Name == "" {
if c.Command.Name == "" { targetFlags = c.App.Flags
targetFlags = c.App.Flags }
} if err := altsrc.ApplyInputSourceValues(c, inputSource, targetFlags); err != nil {
err := altsrc.ApplyInputSourceValues(c, inputSource, targetFlags) log.Errorf("Cannot load configuration from %s: %v", inputSource.Source(), err)
if err != nil { return err
logger.Errorf("Cannot apply configuration from %s: %s", configFile, err)
return err
}
logger.Debugf("Applied configuration from %s", configFile)
} }
return nil return nil
} }

View File

@ -96,7 +96,7 @@ func dnsProxyStandAlone(c *cli.Context) bool {
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) { func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
originCertPath := c.String("origincert") originCertPath := c.String("origincert")
if originCertPath == "" { if originCertPath == "" {
logger.Infof(config.ErrNoConfigFile.Error()) logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
if isRunningFromTerminal() { 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) 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 "", 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")

View File

@ -8,15 +8,16 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/certutil" "github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore" "github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
) )
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to // subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
@ -143,6 +144,18 @@ func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string,
return "", fmt.Errorf("Tunnel credentials file not found") return "", fmt.Errorf("Tunnel credentials file not found")
} }
// getConfigFileTunnelRef returns tunnel UUID or name set in the configuration file
func (sc *subcommandContext) getConfigFileTunnelRef() (string, error) {
if src, err := config.GetConfigFileSource(sc.c, sc.logger); err == nil {
if tunnelRef, err := src.String("tunnel"); err != nil {
return "", errors.Wrapf(err, "invalid tunnel ID or name")
} else {
return tunnelRef, nil
}
}
return "", nil
}
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) { func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
client, err := sc.client() client, err := sc.client()
if err != nil { if err != nil {

View File

@ -315,9 +315,10 @@ func buildRunCommand() *cli.Command {
Action: cliutil.ErrorHandler(runCommand), Action: cliutil.ErrorHandler(runCommand),
Before: SetFlagsFromConfigFile, Before: SetFlagsFromConfigFile,
Usage: "Proxy a local web server by running the given tunnel", Usage: "Proxy a local web server by running the given tunnel",
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] TUNNEL", UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
between your server and the Cloudflare edge. between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the
last command line argument or in the configuration file using "tunnel: TUNNEL".
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run, This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID. however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID.
@ -335,14 +336,32 @@ func runCommand(c *cli.Context) error {
return err return err
} }
if c.NArg() != 1 { if c.NArg() > 1 {
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID or name of the tunnel to run.`) return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`)
} }
tunnelID, err := sc.findID(c.Args().First()) tunnelRef := c.Args().First()
if tunnelRef == "" {
// attempt to read from the config file
if tunnelRef, err = sc.getConfigFileTunnelRef(); err != nil {
return err
}
if tunnelRef == "" {
return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`)
}
}
return runNamedTunnel(sc, tunnelRef)
}
func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
tunnelID, err := sc.findID(tunnelRef)
if err != nil { if err != nil {
return errors.Wrap(err, "error parsing tunnel ID") return errors.Wrap(err, "error parsing tunnel ID")
} }
sc.logger.Infof("Starting tunnel %s", tunnelID.String())
return sc.run(tunnelID) return sc.run(tunnelID)
} }