mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-27 15:19:58 +00:00
Split out typed config from legacy command-line switches; refactor ingress commands and fix tests
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
110
cmd/cloudflared/tunnel/ingress_subcommands.go
Normal file
110
cmd/cloudflared/tunnel/ingress_subcommands.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user