From 051908aaef9c9cadc761e496f99ee4f9d46d3ab9 Mon Sep 17 00:00:00 2001 From: Igor Postelnik Date: Thu, 15 Oct 2020 15:08:57 -0500 Subject: [PATCH] TUN-3463: Let users run a named tunnel via config file setting --- cmd/cloudflared/config/configuration.go | 32 ++++++++- cmd/cloudflared/tunnel/cmd.go | 72 +++++++++----------- cmd/cloudflared/tunnel/configuration.go | 2 +- cmd/cloudflared/tunnel/subcommand_context.go | 19 +++++- cmd/cloudflared/tunnel/subcommands.go | 29 ++++++-- 5 files changed, 104 insertions(+), 50 deletions(-) diff --git a/cmd/cloudflared/config/configuration.go b/cmd/cloudflared/config/configuration.go index 344a09fe..e82b72d1 100644 --- a/cmd/cloudflared/config/configuration.go +++ b/cmd/cloudflared/config/configuration.go @@ -10,9 +10,11 @@ import ( homedir "github.com/mitchellh/go-homedir" "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" "gopkg.in/yaml.v2" "github.com/cloudflare/cloudflared/ingress" + "github.com/cloudflare/cloudflared/logger" "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) { configFilePath := c.String("config") if configFilePath == "" { - return ingress.Ingress{}, ErrNoConfigFile + return ingress.Ingress{}, ingress.ErrNoIngressRules } fmt.Printf("Reading from config file %s\n", configFilePath) configBytes, err := ioutil.ReadFile(configFilePath) @@ -209,3 +211,31 @@ func ReadRules(c *cli.Context) (ingress.Ingress, error) { rules, err := ingress.ParseIngress(configBytes) 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 +} diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index a54fd686..89c65361 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -199,24 +199,29 @@ func buildIngressSubcommand() *cli.Command { } func TunnelCommand(c *cli.Context) error { - if name := c.String("name"); name != "" { // Start a named tunnel - return adhocNamedTunnel(c, name) - } else { // Start a classic tunnel - return classicTunnel(c) + sc, err := newSubcommandContext(c) + if err != nil { + return err } + 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{}) { version, shutdownC, graceShutdownC = v, s, g } -// adhocNamedTunnel create, route and run a named tunnel in one command -func adhocNamedTunnel(c *cli.Context, name string) error { - sc, err := newSubcommandContext(c) - if err != nil { - return err - } - +// runAdhocNamedTunnel create, route and run a named tunnel in one command +func runAdhocNamedTunnel(sc *subcommandContext, name string) error { tunnel, ok, err := sc.tunnelActive(name) if err != nil || !ok { 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) } - if r, ok := routeFromFlag(c); ok { + if r, ok := routeFromFlag(sc.c); ok { if res, err := sc.route(tunnel.ID, r); err != nil { sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err) } else { @@ -242,14 +247,9 @@ func adhocNamedTunnel(c *cli.Context, name string) error { return nil } -// classicTunnel creates a "classic" non-named tunnel -func classicTunnel(c *cli.Context) error { - sc, err := newSubcommandContext(c) - if err != nil { - return err - } - - return StartServer(c, version, shutdownC, graceShutdownC, nil, sc.logger, sc.isUIEnabled) +// runClassicTunnel creates a "classic" non-named tunnel +func runClassicTunnel(sc *subcommandContext) error { + return StartServer(sc.c, version, shutdownC, graceShutdownC, nil, sc.logger, sc.isUIEnabled) } 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 { - logger, err := createLogger(c, false, false) + log, err := createLogger(c, false, false) if err != nil { return cliutil.PrintLoggerSetupError("error setting up logger", err) } - configFile := c.String("config") - if configFile == "" { - logger.Debugf(config.ErrNoConfigFile.Error()) - return nil - } - - inputSource, err := altsrc.NewYamlSourceFromFile(configFile) + inputSource, err := config.GetConfigFileSource(c, log) if err != nil { - logger.Errorf("Cannot load configuration from %s: %s", configFile, err) + if err == config.ErrNoConfigFile { + return nil + } return err } - if inputSource != nil { - targetFlags := c.Command.Flags - if c.Command.Name == "" { - targetFlags = c.App.Flags - } - err := altsrc.ApplyInputSourceValues(c, inputSource, targetFlags) - if err != nil { - logger.Errorf("Cannot apply configuration from %s: %s", configFile, err) - return err - } - logger.Debugf("Applied configuration from %s", configFile) + targetFlags := c.Command.Flags + if c.Command.Name == "" { + targetFlags = c.App.Flags + } + if err := altsrc.ApplyInputSourceValues(c, inputSource, targetFlags); err != nil { + log.Errorf("Cannot load configuration from %s: %v", inputSource.Source(), err) + return err } return nil } diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index b67930ef..5bc1be0e 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -96,7 +96,7 @@ func dnsProxyStandAlone(c *cli.Context) bool { func findOriginCert(c *cli.Context, logger logger.Service) (string, error) { originCertPath := c.String("origincert") 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() { 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") diff --git a/cmd/cloudflared/tunnel/subcommand_context.go b/cmd/cloudflared/tunnel/subcommand_context.go index ae49c2fe..d3b65d13 100644 --- a/cmd/cloudflared/tunnel/subcommand_context.go +++ b/cmd/cloudflared/tunnel/subcommand_context.go @@ -8,15 +8,16 @@ import ( "path/filepath" "strings" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + "github.com/cloudflare/cloudflared/certutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "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 @@ -143,6 +144,18 @@ func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, 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) { client, err := sc.client() if err != nil { diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go index a3162f85..7c51057d 100644 --- a/cmd/cloudflared/tunnel/subcommands.go +++ b/cmd/cloudflared/tunnel/subcommands.go @@ -315,9 +315,10 @@ func buildRunCommand() *cli.Command { Action: cliutil.ErrorHandler(runCommand), Before: SetFlagsFromConfigFile, 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 - 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, 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 } - if c.NArg() != 1 { - return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID or name of the tunnel to run.`) + if c.NArg() > 1 { + 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 { return errors.Wrap(err, "error parsing tunnel ID") } + sc.logger.Infof("Starting tunnel %s", tunnelID.String()) + return sc.run(tunnelID) }