Split out typed config from legacy command-line switches; refactor ingress commands and fix tests
This commit is contained in:
		
							parent
							
								
									eaf03305bd
								
							
						
					
					
						commit
						ca4887fb19
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,6 @@ var (
 | 
			
		|||
	ErrNoIngressRules             = errors.New("No ingress rules were specified in the config file")
 | 
			
		||||
	errLastRuleNotCatchAll        = errors.New("The last ingress rule must match all hostnames (i.e. it must be missing, or must be \"*\")")
 | 
			
		||||
	errBadWildcard                = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"")
 | 
			
		||||
	errNoIngressRulesMatch        = errors.New("The URL didn't match any ingress rules")
 | 
			
		||||
	ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -166,14 +165,3 @@ func ParseIngress(ing UnvalidatedIngress) (Ingress, error) {
 | 
			
		|||
	}
 | 
			
		||||
	return ing.validate()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RuleCommand checks which ingress rule matches the given request URL.
 | 
			
		||||
func RuleCommand(ing Ingress, requestURL *url.URL) error {
 | 
			
		||||
	if requestURL.Hostname() == "" {
 | 
			
		||||
		return fmt.Errorf("%s is malformed and doesn't have a hostname", requestURL)
 | 
			
		||||
	}
 | 
			
		||||
	i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
 | 
			
		||||
	fmt.Printf("Matched rule #%d\n", i+1)
 | 
			
		||||
	fmt.Println(ing.Rules[i].MultiLineString())
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,185 +2,165 @@ package ingress
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	//"reflect"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
	"gopkg.in/yaml.v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//func Test_parseIngress(t *testing.T) {
 | 
			
		||||
//	localhost8000, err := url.Parse("https://localhost:8000")
 | 
			
		||||
//	require.NoError(t, err)
 | 
			
		||||
//	localhost8001, err := url.Parse("https://localhost:8001")
 | 
			
		||||
//	require.NoError(t, err)
 | 
			
		||||
//	type args struct {
 | 
			
		||||
//		rawYAML string
 | 
			
		||||
//	}
 | 
			
		||||
//	tests := []struct {
 | 
			
		||||
//		name    string
 | 
			
		||||
//		args    args
 | 
			
		||||
//		want    Ingress
 | 
			
		||||
//		wantErr bool
 | 
			
		||||
//	}{
 | 
			
		||||
//		{
 | 
			
		||||
//			name:    "Empty file",
 | 
			
		||||
//			args:    args{rawYAML: ""},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Multiple rules",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: tunnel1.example.com
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//  - hostname: "*"
 | 
			
		||||
//    service: https://localhost:8001
 | 
			
		||||
//`},
 | 
			
		||||
//			want: Ingress{Rules: []Rule{
 | 
			
		||||
//				{
 | 
			
		||||
//					Hostname: "tunnel1.example.com",
 | 
			
		||||
//					Service:  localhost8000,
 | 
			
		||||
//				},
 | 
			
		||||
//				{
 | 
			
		||||
//					Hostname: "*",
 | 
			
		||||
//					Service:  localhost8001,
 | 
			
		||||
//				},
 | 
			
		||||
//			}},
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Extra keys",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: "*"
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//extraKey: extraValue
 | 
			
		||||
//`},
 | 
			
		||||
//			want: Ingress{Rules: []Rule{
 | 
			
		||||
//				{
 | 
			
		||||
//					Hostname: "*",
 | 
			
		||||
//					Service:  localhost8000,
 | 
			
		||||
//				},
 | 
			
		||||
//			}},
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Hostname can be omitted",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - service: https://localhost:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			want: Ingress{Rules: []Rule{
 | 
			
		||||
//				{
 | 
			
		||||
//					Service: localhost8000,
 | 
			
		||||
//				},
 | 
			
		||||
//			}},
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Invalid service",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: "*"
 | 
			
		||||
//    service: https://local host:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Invalid YAML",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//key: "value
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Last rule isn't catchall",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: example.com
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "First rule is catchall",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - service: https://localhost:8000
 | 
			
		||||
//  - hostname: example.com
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Catch-all rule can't have a path",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - service: https://localhost:8001
 | 
			
		||||
//    path: /subpath1/(.*)/subpath2
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Invalid regex",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: example.com
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//    path: "*/subpath2"
 | 
			
		||||
//  - service: https://localhost:8001
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Service must have a scheme",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - service: localhost:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Wildcard not at start",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: "test.*.example.com"
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Can't use --url",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//url: localhost:8080
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: "*.example.com"
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//		{
 | 
			
		||||
//			name: "Service can't have a path",
 | 
			
		||||
//			args: args{rawYAML: `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - service: https://localhost:8000/static/
 | 
			
		||||
//`},
 | 
			
		||||
//			wantErr: true,
 | 
			
		||||
//		},
 | 
			
		||||
//	}
 | 
			
		||||
//	for _, tt := range tests {
 | 
			
		||||
//		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
//			got, err := ParseIngress([]byte(tt.args.rawYAML))
 | 
			
		||||
//			if (err != nil) != tt.wantErr {
 | 
			
		||||
//				t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
//				return
 | 
			
		||||
//			}
 | 
			
		||||
//			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
//				t.Errorf("ParseIngress() = %v, want %v", got, tt.want)
 | 
			
		||||
//			}
 | 
			
		||||
//		})
 | 
			
		||||
//	}
 | 
			
		||||
//}
 | 
			
		||||
func Test_parseIngress(t *testing.T) {
 | 
			
		||||
	localhost8000 := MustParseURL(t, "https://localhost:8000")
 | 
			
		||||
	localhost8001 := MustParseURL(t, "https://localhost:8001")
 | 
			
		||||
	type args struct {
 | 
			
		||||
		rawYAML string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    Ingress
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:    "Empty file",
 | 
			
		||||
			args:    args{rawYAML: ""},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Multiple rules",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: tunnel1.example.com
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
 - hostname: "*"
 | 
			
		||||
   service: https://localhost:8001
 | 
			
		||||
`},
 | 
			
		||||
			want: Ingress{Rules: []Rule{
 | 
			
		||||
				{
 | 
			
		||||
					Hostname: "tunnel1.example.com",
 | 
			
		||||
					Service:  localhost8000,
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Hostname: "*",
 | 
			
		||||
					Service:  localhost8001,
 | 
			
		||||
				},
 | 
			
		||||
			}},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Extra keys",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: "*"
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
extraKey: extraValue
 | 
			
		||||
`},
 | 
			
		||||
			want: Ingress{Rules: []Rule{
 | 
			
		||||
				{
 | 
			
		||||
					Hostname: "*",
 | 
			
		||||
					Service:  localhost8000,
 | 
			
		||||
				},
 | 
			
		||||
			}},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Hostname can be omitted",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - service: https://localhost:8000
 | 
			
		||||
`},
 | 
			
		||||
			want: Ingress{Rules: []Rule{
 | 
			
		||||
				{
 | 
			
		||||
					Service: localhost8000,
 | 
			
		||||
				},
 | 
			
		||||
			}},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Invalid service",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: "*"
 | 
			
		||||
   service: https://local host:8000
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Last rule isn't catchall",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: example.com
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "First rule is catchall",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - service: https://localhost:8000
 | 
			
		||||
 - hostname: example.com
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Catch-all rule can't have a path",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - service: https://localhost:8001
 | 
			
		||||
   path: /subpath1/(.*)/subpath2
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Invalid regex",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: example.com
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
   path: "*/subpath2"
 | 
			
		||||
 - service: https://localhost:8001
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Service must have a scheme",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - service: localhost:8000
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Wildcard not at start",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: "test.*.example.com"
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Service can't have a path",
 | 
			
		||||
			args: args{rawYAML: `
 | 
			
		||||
ingress:
 | 
			
		||||
 - service: https://localhost:8000/static/
 | 
			
		||||
`},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got, err := ParseIngress(MustReadIngress(tt.args.rawYAML))
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			assert.Equal(t, tt.want, got)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustParse(t *testing.T, rawURL string) *url.URL {
 | 
			
		||||
func MustParseURL(t *testing.T, rawURL string) *url.URL {
 | 
			
		||||
	u, err := url.Parse(rawURL)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	return u
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +187,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "example.com",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://example.com"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://example.com"),
 | 
			
		||||
			},
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -217,7 +197,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "*",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://example.com"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://example.com"),
 | 
			
		||||
			},
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -227,7 +207,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "example.com",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://foo.bar"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://foo.bar"),
 | 
			
		||||
			},
 | 
			
		||||
			want: false,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -237,7 +217,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "*.example.com",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://adam.example.com"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://adam.example.com"),
 | 
			
		||||
			},
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -247,7 +227,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "*.example.com",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://tunnel.com"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://tunnel.com"),
 | 
			
		||||
			},
 | 
			
		||||
			want: false,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +237,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "*example.com",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://www.example.com"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://www.example.com"),
 | 
			
		||||
			},
 | 
			
		||||
			want: false,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -267,7 +247,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Hostname: "*.example.com",
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://adam.chalmers.example.com"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://adam.chalmers.example.com"),
 | 
			
		||||
			},
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -278,7 +258,7 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
				Path:     regexp.MustCompile("/static/.*\\.html"),
 | 
			
		||||
			},
 | 
			
		||||
			args: args{
 | 
			
		||||
				requestURL: MustParse(t, "https://www.example.com/static/index.html"),
 | 
			
		||||
				requestURL: MustParseURL(t, "https://www.example.com/static/index.html"),
 | 
			
		||||
			},
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -298,23 +278,33 @@ func Test_rule_matches(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//func BenchmarkFindMatch(b *testing.B) {
 | 
			
		||||
//	rulesYAML := `
 | 
			
		||||
//ingress:
 | 
			
		||||
//  - hostname: tunnel1.example.com
 | 
			
		||||
//    service: https://localhost:8000
 | 
			
		||||
//  - hostname: tunnel2.example.com
 | 
			
		||||
//    service: https://localhost:8001
 | 
			
		||||
//  - hostname: "*"
 | 
			
		||||
//    service: https://localhost:8002
 | 
			
		||||
//`
 | 
			
		||||
//	ing, err := ParseIngress([]byte(rulesYAML))
 | 
			
		||||
//	if err != nil {
 | 
			
		||||
//		b.Error(err)
 | 
			
		||||
//	}
 | 
			
		||||
//	for n := 0; n < b.N; n++ {
 | 
			
		||||
//		ing.FindMatchingRule("tunnel1.example.com", "")
 | 
			
		||||
//		ing.FindMatchingRule("tunnel2.example.com", "")
 | 
			
		||||
//		ing.FindMatchingRule("tunnel3.example.com", "")
 | 
			
		||||
//	}
 | 
			
		||||
//}
 | 
			
		||||
func BenchmarkFindMatch(b *testing.B) {
 | 
			
		||||
	rulesYAML := `
 | 
			
		||||
ingress:
 | 
			
		||||
 - hostname: tunnel1.example.com
 | 
			
		||||
   service: https://localhost:8000
 | 
			
		||||
 - hostname: tunnel2.example.com
 | 
			
		||||
   service: https://localhost:8001
 | 
			
		||||
 - hostname: "*"
 | 
			
		||||
   service: https://localhost:8002
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
	ing, err := ParseIngress(MustReadIngress(rulesYAML))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Error(err)
 | 
			
		||||
	}
 | 
			
		||||
	for n := 0; n < b.N; n++ {
 | 
			
		||||
		ing.FindMatchingRule("tunnel1.example.com", "")
 | 
			
		||||
		ing.FindMatchingRule("tunnel2.example.com", "")
 | 
			
		||||
		ing.FindMatchingRule("tunnel3.example.com", "")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustReadIngress(s string) UnvalidatedIngress {
 | 
			
		||||
	var ing UnvalidatedIngress
 | 
			
		||||
	err := yaml.Unmarshal([]byte(s), &ing)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return ing
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue