Split out typed config from legacy command-line switches; refactor ingress commands and fix tests

This commit is contained in:
Igor Postelnik
2020-10-20 09:29:13 -05:00
parent eaf03305bd
commit ca4887fb19
6 changed files with 326 additions and 347 deletions

View File

@@ -197,22 +197,23 @@ func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
return validUrl, err
}
func ReadIngressRules(config *ConfigFileSettings) (ingress.Ingress, error) {
return ingress.ParseIngress(config.Ingress)
}
type ConfigFileSettings struct {
type Configuration struct {
TunnelID string `yaml:"tunnel"`
Ingress ingress.UnvalidatedIngress `yaml:",inline"`
Settings map[string]interface{} `yaml:",inline"`
sourceFile string
}
func (c *ConfigFileSettings) Source() string {
type configFileSettings struct {
Configuration `yaml:",inline"`
// Existing settings will be aggregated in the generic map, should be read via cli.Context
Settings map[string]interface{} `yaml:",inline"`
}
func (c *Configuration) Source() string {
return c.sourceFile
}
func (c *ConfigFileSettings) Int(name string) (int, error) {
func (c *configFileSettings) Int(name string) (int, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(int); ok {
return v, nil
@@ -222,7 +223,7 @@ func (c *ConfigFileSettings) Int(name string) (int, error) {
return 0, nil
}
func (c *ConfigFileSettings) Duration(name string) (time.Duration, error) {
func (c *configFileSettings) Duration(name string) (time.Duration, error) {
if raw, ok := c.Settings[name]; ok {
switch v := raw.(type) {
case time.Duration:
@@ -235,70 +236,70 @@ func (c *ConfigFileSettings) Duration(name string) (time.Duration, error) {
return 0, nil
}
func (c *ConfigFileSettings) Float64(name string) (float64, error) {
func (c *configFileSettings) Float64(name string) (float64, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(float64); ok {
return v, nil
}
return 0, fmt.Errorf("expected int found %T for %s", raw, name)
return 0, fmt.Errorf("expected float found %T for %s", raw, name)
}
return 0, nil
}
func (c *ConfigFileSettings) String(name string) (string, error) {
func (c *configFileSettings) String(name string) (string, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(string); ok {
return v, nil
}
return "", fmt.Errorf("expected int found %T for %s", raw, name)
return "", fmt.Errorf("expected string found %T for %s", raw, name)
}
return "", nil
}
func (c *ConfigFileSettings) StringSlice(name string) ([]string, error) {
func (c *configFileSettings) StringSlice(name string) ([]string, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.([]string); ok {
return v, nil
}
return nil, fmt.Errorf("expected int found %T for %s", raw, name)
return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
}
return nil, nil
}
func (c *ConfigFileSettings) IntSlice(name string) ([]int, error) {
func (c *configFileSettings) IntSlice(name string) ([]int, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.([]int); ok {
return v, nil
}
return nil, fmt.Errorf("expected int found %T for %s", raw, name)
return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
}
return nil, nil
}
func (c *ConfigFileSettings) Generic(name string) (cli.Generic, error) {
func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
return nil, errors.New("option type Generic not supported")
}
func (c *ConfigFileSettings) Bool(name string) (bool, error) {
func (c *configFileSettings) Bool(name string) (bool, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(bool); ok {
return v, nil
}
return false, fmt.Errorf("expected int found %T for %s", raw, name)
return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
}
return false, nil
}
var configuration ConfigFileSettings
var configuration configFileSettings
func GetConfiguration() *ConfigFileSettings {
return &configuration
func GetConfiguration() *Configuration {
return &configuration.Configuration
}
// ReadConfigFile 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 ReadConfigFile(c *cli.Context, log logger.Service) (*ConfigFileSettings, error) {
func ReadConfigFile(c *cli.Context, log logger.Service) (*configFileSettings, error) {
configFile := c.String("config")
if configuration.Source() == configFile || configFile == "" {
return &configuration, nil

View File

@@ -26,7 +26,6 @@ import (
"github.com/cloudflare/cloudflared/dbconnect"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin"
@@ -167,37 +166,6 @@ func buildTunnelCommand(subcommands []*cli.Command) *cli.Command {
}
}
func buildIngressSubcommand() *cli.Command {
return &cli.Command{
Name: "ingress",
Category: "Tunnel",
Usage: "Validate and test cloudflared tunnel's ingress configuration",
Hidden: true,
Description: `
Cloudflared lets you route traffic from the internet to multiple different addresses on your
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
by its hostname or path, and routes it to an address. These rules are configured under the
'ingress' key of your config.yaml, for example:
ingress:
- hostname: www.example.com
service: https://localhost:8000
- hostname: *.example.xyz
path: /[a-zA-Z]+.html
service: https://localhost:8001
- hostname: *
service: https://localhost:8002
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
rule that matches all traffic. You can validate these rules with the 'ingress validate'
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
Multiple-origin routing is incompatible with the --url flag.`,
Subcommands: []*cli.Command{buildValidateCommand(), buildRuleCommand()},
Flags: tunnelFlags(false),
}
}
func TunnelCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
@@ -1239,81 +1207,3 @@ reconnect [delay]
}
}
}
func buildValidateCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(ValidateCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
}
}
func buildRuleCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(RuleCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
ArgsUsage: "URL",
Description: "Check which ingress rule matches a given request URL. " +
"Ingress rules match a request's hostname and path. Hostname is " +
"optional and is either a full hostname like `www.example.com` or a " +
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
}
}
// Validates the ingress rules in the cloudflared config file
func ValidateCommand(c *cli.Context) error {
logger, err := createLogger(c, false, false)
if err != nil {
return err
}
configFile, err := config.ReadConfigFile(c, logger)
if err != nil {
return err
}
fmt.Println("Validating rules from", configFile.Source())
_, err = config.ReadIngressRules(configFile)
if err != nil {
return errors.Wrap(err, "Validation failed")
}
if c.IsSet("url") {
return ingress.ErrURLIncompatibleWithIngress
}
fmt.Println("OK")
return nil
}
// Checks which ingress rule matches the given URL.
func RuleCommand(c *cli.Context) error {
logger, err := createLogger(c, false, false)
if err != nil {
return err
}
configFile, err := config.ReadConfigFile(c, logger)
if err != nil {
return err
}
rules, err := config.ReadIngressRules(configFile)
if err != nil {
return err
}
requestArg := c.Args().First()
if requestArg == "" {
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
}
requestURL, err := url.Parse(requestArg)
if err != nil {
return fmt.Errorf("%s is not a valid URL", requestArg)
}
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
}
if requestURL.Hostname() == "" {
return fmt.Errorf("%s doesn't have a hostname", requestArg)
}
return ingress.RuleCommand(rules, requestURL)
}

View File

@@ -231,7 +231,7 @@ func prepareTunnelConfig(
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
}
ingressRules, err = config.ReadIngressRules(config.GetConfiguration())
ingressRules, err = ingress.ParseIngress(config.GetConfiguration().Ingress)
if err != nil && err != ingress.ErrNoIngressRules {
return nil, err
}

View File

@@ -0,0 +1,110 @@
package tunnel
import (
"fmt"
"net/url"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress"
)
func buildIngressSubcommand() *cli.Command {
return &cli.Command{
Name: "ingress",
Category: "Tunnel",
Usage: "Validate and test cloudflared tunnel's ingress configuration",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]",
Hidden: true,
Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
by its hostname or path, and routes it to an address. These rules are configured under the
'ingress' key of your config.yaml, for example:
ingress:
- hostname: www.example.com
service: https://localhost:8000
- hostname: *.example.xyz
path: /[a-zA-Z]+.html
service: https://localhost:8001
- hostname: *
service: https://localhost:8002
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
rule that matches all traffic. You can validate these rules with the 'ingress validate'
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
Multiple-origin routing is incompatible with the --url flag.`,
Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()},
}
}
func buildValidateIngressCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(validateIngressCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
}
}
func buildTestURLCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(testURLCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
ArgsUsage: "URL",
Description: "Check which ingress rule matches a given request URL. " +
"Ingress rules match a request's hostname and path. Hostname is " +
"optional and is either a full hostname like `www.example.com` or a " +
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
}
}
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
func validateIngressCommand(c *cli.Context) error {
conf := config.GetConfiguration()
fmt.Println("Validating rules from", conf.Source())
if _, err := ingress.ParseIngress(conf.Ingress); err != nil {
return errors.Wrap(err, "Validation failed")
}
if c.IsSet("url") {
return ingress.ErrURLIncompatibleWithIngress
}
fmt.Println("OK")
return nil
}
// testURLCommand checks which ingress rule matches the given URL.
func testURLCommand(c *cli.Context) error {
requestArg := c.Args().First()
if requestArg == "" {
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
}
requestURL, err := url.Parse(requestArg)
if err != nil {
return fmt.Errorf("%s is not a valid URL", requestArg)
}
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
}
conf := config.GetConfiguration()
fmt.Println("Using rules from", conf.Source())
ing, err := ingress.ParseIngress(conf.Ingress)
if err != nil {
return errors.Wrap(err, "Validation failed")
}
i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
fmt.Printf("Matched rule #%d\n", i+1)
fmt.Println(ing.Rules[i].MultiLineString())
return nil
}