New static configuration loading system.

Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
This commit is contained in:
Ludovic Fernandez 2019-06-17 11:48:05 +02:00 committed by Traefiker Bot
parent d18edd6f77
commit 8d7eccad5d
165 changed files with 10894 additions and 6076 deletions

1
.gitignore vendored
View file

@ -11,6 +11,7 @@
/autogen/ /autogen/
/traefik /traefik
/traefik.toml /traefik.toml
/traefik.yml
*.log *.log
*.exe *.exe
cover.out cover.out

View file

@ -36,6 +36,8 @@
"scopelint", "scopelint",
"gochecknoinits", "gochecknoinits",
"gochecknoglobals", "gochecknoglobals",
# uncomment when the CI will be updated
# "bodyclose", # Too many false-positive and panics.
] ]
[issues] [issues]
@ -72,7 +74,7 @@
path = "pkg/provider/kubernetes/builder_(endpoint|service)_test.go" path = "pkg/provider/kubernetes/builder_(endpoint|service)_test.go"
text = "(U1000: func )?`(.+)` is unused" text = "(U1000: func )?`(.+)` is unused"
[[issues.exclude-rules]] [[issues.exclude-rules]]
path = "pkg/provider/label/internal/.+_test.go" path = "pkg/config/parser/.+_test.go"
text = "U1000: field `(foo|fuu)` is unused" text = "U1000: field `(foo|fuu)` is unused"
[[issues.exclude-rules]] [[issues.exclude-rules]]
path = "pkg/server/service/bufferpool.go" path = "pkg/server/service/bufferpool.go"

39
Gopkg.lock generated
View file

@ -151,12 +151,9 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:28be1959f81e9a6dec3058768a4c4535cf73fcd6e171d21688ad0a7fdf49d43a" digest = "1:d37d0fa13c422a0b85981fe42ff8f176885921294cf0c3ce585c160669cc32bb"
name = "github.com/abronan/valkeyrie" name = "github.com/abronan/valkeyrie"
packages = [ packages = ["store"]
".",
"store",
]
pruneopts = "NUT" pruneopts = "NUT"
revision = "063d875e3c5fd734fa2aa12fac83829f62acfc70" revision = "063d875e3c5fd734fa2aa12fac83829f62acfc70"
@ -295,17 +292,6 @@
pruneopts = "NUT" pruneopts = "NUT"
revision = "d83ebdd94cbdbcd9c6c6a22e1a0cde05e55d9d90" revision = "d83ebdd94cbdbcd9c6c6a22e1a0cde05e55d9d90"
[[projects]]
digest = "1:3cd675d508d3f9067704d36011c7a262fc0a0bf1ad0361a7d1e60a405d12941e"
name = "github.com/containous/flaeg"
packages = [
".",
"parse",
]
pruneopts = "NUT"
revision = "c93d194b807ef171c43344d60adad8b58217390a"
version = "v1.4.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:3a789aa5487458c1fc913b47be763e5906e1524f1143acb8617287866184f9a7" digest = "1:3a789aa5487458c1fc913b47be763e5906e1524f1143acb8617287866184f9a7"
@ -314,14 +300,6 @@
pruneopts = "NUT" pruneopts = "NUT"
revision = "c33f32e268983f989290677351b871b65da75ba5" revision = "c33f32e268983f989290677351b871b65da75ba5"
[[projects]]
digest = "1:a4f16a1b72349621b887bde88f458da518160abcb600eae3d591d8a2afa78bda"
name = "github.com/containous/staert"
packages = ["."]
pruneopts = "NUT"
revision = "7a9987c3a6d46be84e141a5c3191347ec10af17d"
version = "v3.1.2"
[[projects]] [[projects]]
digest = "1:fa91847d50d3f656fc2d2d608b9749b97d77528e8988ad8001f957640545e91e" digest = "1:fa91847d50d3f656fc2d2d608b9749b97d77528e8988ad8001f957640545e91e"
name = "github.com/coreos/go-systemd" name = "github.com/coreos/go-systemd"
@ -1228,14 +1206,6 @@
revision = "d8152159450570012552f924a0ae6ab3d8c617e0" revision = "d8152159450570012552f924a0ae6ab3d8c617e0"
version = "v0.6.0" version = "v0.6.0"
[[projects]]
branch = "master"
digest = "1:95d27e49401b61dd203a4cf8237037bd6cd49599651f855ac1988c4ae27b090e"
name = "github.com/ogier/pflag"
packages = ["."]
pruneopts = "NUT"
revision = "45c278ab3607870051a2ea9040bb85fcb8557481"
[[projects]] [[projects]]
digest = "1:69e47979091e47a10e5ff0e2776ca71aa3e884238ce446bd71e246878ba0858d" digest = "1:69e47979091e47a10e5ff0e2776ca71aa3e884238ce446bd71e246878ba0858d"
name = "github.com/opencontainers/go-digest" name = "github.com/opencontainers/go-digest"
@ -2262,10 +2232,7 @@
"github.com/armon/go-proxyproto", "github.com/armon/go-proxyproto",
"github.com/cenkalti/backoff", "github.com/cenkalti/backoff",
"github.com/containous/alice", "github.com/containous/alice",
"github.com/containous/flaeg",
"github.com/containous/flaeg/parse",
"github.com/containous/mux", "github.com/containous/mux",
"github.com/containous/staert",
"github.com/coreos/go-systemd/daemon", "github.com/coreos/go-systemd/daemon",
"github.com/davecgh/go-spew/spew", "github.com/davecgh/go-spew/spew",
"github.com/docker/docker/api/types", "github.com/docker/docker/api/types",
@ -2312,7 +2279,6 @@
"github.com/mitchellh/copystructure", "github.com/mitchellh/copystructure",
"github.com/mitchellh/hashstructure", "github.com/mitchellh/hashstructure",
"github.com/mvdan/xurls", "github.com/mvdan/xurls",
"github.com/ogier/pflag",
"github.com/opentracing/opentracing-go", "github.com/opentracing/opentracing-go",
"github.com/opentracing/opentracing-go/ext", "github.com/opentracing/opentracing-go/ext",
"github.com/opentracing/opentracing-go/log", "github.com/opentracing/opentracing-go/log",
@ -2353,6 +2319,7 @@
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer", "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer",
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer", "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer",
"gopkg.in/fsnotify.v1", "gopkg.in/fsnotify.v1",
"gopkg.in/yaml.v2",
"k8s.io/api/core/v1", "k8s.io/api/core/v1",
"k8s.io/api/extensions/v1beta1", "k8s.io/api/extensions/v1beta1",
"k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/api/errors",

View file

@ -69,10 +69,6 @@ required = [
name = "github.com/cenkalti/backoff" name = "github.com/cenkalti/backoff"
version = "2.1.1" version = "2.1.1"
[[constraint]]
name = "github.com/containous/flaeg"
version = "1.4.1"
[[constraint]] [[constraint]]
branch = "master" branch = "master"
name = "github.com/containous/mux" name = "github.com/containous/mux"
@ -81,14 +77,10 @@ required = [
branch = "containous-fork" branch = "containous-fork"
name = "github.com/containous/alice" name = "github.com/containous/alice"
[[constraint]] #[[constraint]]
name = "github.com/containous/staert" # name = "github.com/thoas/stats"
version = "3.1.2" # # related to https://github.com/thoas/stats/pull/32
# revision = "4975baf6a358ed3ddaa42133996e1959f96c9300"
[[constraint]]
name = "github.com/thoas/stats"
# related to https://github.com/thoas/stats/pull/32
revision = "4975baf6a358ed3ddaa42133996e1959f96c9300"
[[constraint]] [[constraint]]
name = "github.com/coreos/go-systemd" name = "github.com/coreos/go-systemd"
@ -137,9 +129,9 @@ required = [
# branch = "master" # branch = "master"
# name = "github.com/jjcollinge/servicefabric" # name = "github.com/jjcollinge/servicefabric"
[[constraint]] #[[constraint]]
branch = "master" # branch = "master"
name = "github.com/abronan/valkeyrie" # name = "github.com/abronan/valkeyrie"
#[[constraint]] #[[constraint]]
# name = "github.com/mesosphere/mesos-dns" # name = "github.com/mesosphere/mesos-dns"

View file

@ -3,40 +3,27 @@ package cmd
import ( import (
"time" "time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/middlewares/accesslog"
"github.com/containous/traefik/pkg/ping"
"github.com/containous/traefik/pkg/provider/docker"
"github.com/containous/traefik/pkg/provider/file"
"github.com/containous/traefik/pkg/provider/kubernetes/ingress"
"github.com/containous/traefik/pkg/provider/marathon"
"github.com/containous/traefik/pkg/provider/rancher"
"github.com/containous/traefik/pkg/provider/rest"
"github.com/containous/traefik/pkg/tracing/datadog"
"github.com/containous/traefik/pkg/tracing/instana"
"github.com/containous/traefik/pkg/tracing/jaeger"
"github.com/containous/traefik/pkg/tracing/zipkin"
"github.com/containous/traefik/pkg/types" "github.com/containous/traefik/pkg/types"
jaegercli "github.com/uber/jaeger-client-go"
) )
// TraefikConfiguration holds GlobalConfiguration and other stuff // TraefikCmdConfiguration wraps the static configuration and extra parameters.
type TraefikConfiguration struct { type TraefikCmdConfiguration struct {
static.Configuration `mapstructure:",squash" export:"true"` static.Configuration `export:"true"`
ConfigFile string `short:"c" description:"Configuration file to use (TOML)." export:"true"` // ConfigFile is the path to the configuration file.
ConfigFile string `description:"Configuration file to use. If specified all other flags are ignored." export:"true"`
} }
// NewTraefikConfiguration creates a TraefikConfiguration with default values // NewTraefikConfiguration creates a TraefikCmdConfiguration with default values.
func NewTraefikConfiguration() *TraefikConfiguration { func NewTraefikConfiguration() *TraefikCmdConfiguration {
return &TraefikConfiguration{ return &TraefikCmdConfiguration{
Configuration: static.Configuration{ Configuration: static.Configuration{
Global: &static.Global{ Global: &static.Global{
CheckNewVersion: true, CheckNewVersion: true,
}, },
EntryPoints: make(static.EntryPoints), EntryPoints: make(static.EntryPoints),
Providers: &static.Providers{ Providers: &static.Providers{
ProvidersThrottleDuration: parse.Duration(2 * time.Second), ProvidersThrottleDuration: types.Duration(2 * time.Second),
}, },
ServersTransport: &static.ServersTransport{ ServersTransport: &static.ServersTransport{
MaxIdleConnsPerHost: 200, MaxIdleConnsPerHost: 200,
@ -45,162 +32,3 @@ func NewTraefikConfiguration() *TraefikConfiguration {
ConfigFile: "", ConfigFile: "",
} }
} }
// NewTraefikDefaultPointersConfiguration creates a TraefikConfiguration with pointers default values
func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
// default File
var defaultFile file.Provider
defaultFile.Watch = true
defaultFile.Filename = "" // needs equivalent to viper.ConfigFileUsed()
// default Ping
var defaultPing = ping.Handler{
EntryPoint: "traefik",
}
// default TraefikLog
defaultTraefikLog := types.TraefikLog{
Format: "common",
FilePath: "",
}
// default AccessLog
defaultAccessLog := types.AccessLog{
Format: accesslog.CommonFormat,
FilePath: "",
Filters: &types.AccessLogFilters{},
Fields: &types.AccessLogFields{
DefaultMode: types.AccessLogKeep,
Headers: &types.FieldHeaders{
DefaultMode: types.AccessLogKeep,
},
},
}
// default Tracing
defaultTracing := static.Tracing{
Backend: "jaeger",
ServiceName: "traefik",
SpanNameLimit: 0,
Jaeger: &jaeger.Config{
SamplingServerURL: "http://localhost:5778/sampling",
SamplingType: "const",
SamplingParam: 1.0,
LocalAgentHostPort: "127.0.0.1:6831",
Propagation: "jaeger",
Gen128Bit: false,
TraceContextHeaderName: jaegercli.TraceContextHeaderName,
},
Zipkin: &zipkin.Config{
HTTPEndpoint: "http://localhost:9411/api/v1/spans",
SameSpan: false,
ID128Bit: true,
Debug: false,
SampleRate: 1.0,
},
DataDog: &datadog.Config{
LocalAgentHostPort: "localhost:8126",
GlobalTag: "",
Debug: false,
PrioritySampling: false,
},
Instana: &instana.Config{
LocalAgentHost: "localhost",
LocalAgentPort: 42699,
LogLevel: "info",
},
}
// default ApiConfiguration
defaultAPI := static.API{
EntryPoint: "traefik",
Dashboard: true,
}
defaultAPI.Statistics = &types.Statistics{
RecentErrors: 10,
}
// default Metrics
defaultMetrics := types.Metrics{
Prometheus: &types.Prometheus{
Buckets: types.Buckets{0.1, 0.3, 1.2, 5},
EntryPoint: static.DefaultInternalEntryPointName,
},
Datadog: &types.Datadog{
Address: "localhost:8125",
PushInterval: "10s",
},
StatsD: &types.Statsd{
Address: "localhost:8125",
PushInterval: "10s",
},
InfluxDB: &types.InfluxDB{
Address: "localhost:8089",
Protocol: "udp",
PushInterval: "10s",
},
}
defaultResolver := types.HostResolverConfig{
CnameFlattening: false,
ResolvConfig: "/etc/resolv.conf",
ResolvDepth: 5,
}
var defaultDocker docker.Provider
defaultDocker.Watch = true
defaultDocker.ExposedByDefault = true
defaultDocker.Endpoint = "unix:///var/run/docker.sock"
defaultDocker.SwarmMode = false
defaultDocker.SwarmModeRefreshSeconds = 15
defaultDocker.DefaultRule = docker.DefaultTemplateRule
// default Rest
var defaultRest rest.Provider
defaultRest.EntryPoint = static.DefaultInternalEntryPointName
// default Marathon
var defaultMarathon marathon.Provider
defaultMarathon.Watch = true
defaultMarathon.Endpoint = "http://127.0.0.1:8080"
defaultMarathon.ExposedByDefault = true
defaultMarathon.DialerTimeout = parse.Duration(5 * time.Second)
defaultMarathon.ResponseHeaderTimeout = parse.Duration(60 * time.Second)
defaultMarathon.TLSHandshakeTimeout = parse.Duration(5 * time.Second)
defaultMarathon.KeepAlive = parse.Duration(10 * time.Second)
defaultMarathon.DefaultRule = marathon.DefaultTemplateRule
// default Kubernetes
var defaultKubernetes ingress.Provider
// default Rancher
var defaultRancher rancher.Provider
defaultRancher.Watch = true
defaultRancher.ExposedByDefault = true
defaultRancher.EnableServiceHealthFilter = true
defaultRancher.RefreshSeconds = 15
defaultRancher.DefaultRule = rancher.DefaultTemplateRule
defaultRancher.Prefix = "latest"
defaultProviders := static.Providers{
File: &defaultFile,
Docker: &defaultDocker,
Rest: &defaultRest,
Marathon: &defaultMarathon,
Kubernetes: &defaultKubernetes,
Rancher: &defaultRancher,
}
return &TraefikConfiguration{
Configuration: static.Configuration{
Providers: &defaultProviders,
Log: &defaultTraefikLog,
AccessLog: &defaultAccessLog,
Ping: &defaultPing,
API: &defaultAPI,
Metrics: &defaultMetrics,
Tracing: &defaultTracing,
HostResolver: &defaultResolver,
},
}
}

View file

@ -7,34 +7,34 @@ import (
"os" "os"
"time" "time"
"github.com/containous/flaeg" "github.com/containous/traefik/pkg/cli"
"github.com/containous/traefik/cmd"
"github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/config/static"
) )
// NewCmd builds a new HealthCheck command // NewCmd builds a new HealthCheck command.
func NewCmd(traefikConfiguration *cmd.TraefikConfiguration, traefikPointersConfiguration *cmd.TraefikConfiguration) *flaeg.Command { func NewCmd(traefikConfiguration *static.Configuration, loaders []cli.ResourceLoader) *cli.Command {
return &flaeg.Command{ return &cli.Command{
Name: "healthcheck", Name: "healthcheck",
Description: `Calls traefik /ping to check health (web provider must be enabled)`, Description: `Calls Traefik /ping to check the health of Traefik (the API must be enabled).`,
Config: traefikConfiguration, Configuration: traefikConfiguration,
DefaultPointersConfig: traefikPointersConfiguration,
Run: runCmd(traefikConfiguration), Run: runCmd(traefikConfiguration),
Metadata: map[string]string{ Resources: loaders,
"parseAllSources": "true",
},
} }
} }
func runCmd(traefikConfiguration *cmd.TraefikConfiguration) func() error { func runCmd(traefikConfiguration *static.Configuration) func(_ []string) error {
return func() error { return func(_ []string) error {
traefikConfiguration.Configuration.SetEffectiveConfiguration(traefikConfiguration.ConfigFile) traefikConfiguration.SetEffectiveConfiguration("")
resp, errPing := Do(traefikConfiguration.Configuration) resp, errPing := Do(*traefikConfiguration)
if resp != nil {
resp.Body.Close()
}
if errPing != nil { if errPing != nil {
fmt.Printf("Error calling healthcheck: %s\n", errPing) fmt.Printf("Error calling healthcheck: %s\n", errPing)
os.Exit(1) os.Exit(1)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
fmt.Printf("Bad healthcheck status: %s\n", resp.Status) fmt.Printf("Bad healthcheck status: %s\n", resp.Status)
os.Exit(1) os.Exit(1)
@ -50,6 +50,7 @@ func Do(staticConfiguration static.Configuration) (*http.Response, error) {
if staticConfiguration.Ping == nil { if staticConfiguration.Ping == nil {
return nil, errors.New("please enable `ping` to use health check") return nil, errors.New("please enable `ping` to use health check")
} }
pingEntryPoint, ok := staticConfiguration.EntryPoints[staticConfiguration.Ping.EntryPoint] pingEntryPoint, ok := staticConfiguration.EntryPoints[staticConfiguration.Ping.EntryPoint]
if !ok { if !ok {
return nil, errors.New("missing `ping` entrypoint") return nil, errors.New("missing `ping` entrypoint")

View file

@ -1,150 +0,0 @@
package storeconfig
import (
"encoding/json"
"fmt"
stdlog "log"
"github.com/containous/flaeg"
"github.com/containous/staert"
"github.com/containous/traefik/cmd"
)
// NewCmd builds a new StoreConfig command
func NewCmd(traefikConfiguration *cmd.TraefikConfiguration, traefikPointersConfiguration *cmd.TraefikConfiguration) *flaeg.Command {
return &flaeg.Command{
Name: "storeconfig",
Description: `Stores the static traefik configuration into a Key-value store. Traefik will not start.`,
Config: traefikConfiguration,
DefaultPointersConfig: traefikPointersConfiguration,
HideHelp: true, // TODO storeconfig
Metadata: map[string]string{
"parseAllSources": "true",
},
}
}
// Run store config in KV
func Run(kv *staert.KvSource, traefikConfiguration *cmd.TraefikConfiguration) func() error {
return func() error {
if kv == nil {
return fmt.Errorf("error using command storeconfig, no Key-value store defined")
}
fileConfig := traefikConfiguration.Providers.File
if fileConfig != nil {
traefikConfiguration.Providers.File = nil
if len(fileConfig.Filename) == 0 && len(fileConfig.Directory) == 0 {
fileConfig.Filename = traefikConfiguration.ConfigFile
}
}
jsonConf, err := json.Marshal(traefikConfiguration.Configuration)
if err != nil {
return err
}
stdlog.Printf("Storing configuration: %s\n", jsonConf)
err = kv.StoreConfig(traefikConfiguration.Configuration)
if err != nil {
return err
}
if fileConfig != nil {
jsonConf, err = json.Marshal(fileConfig)
if err != nil {
return err
}
stdlog.Printf("Storing file configuration: %s\n", jsonConf)
config, err := fileConfig.BuildConfiguration()
if err != nil {
return err
}
stdlog.Print("Writing config to KV")
err = kv.StoreConfig(config)
if err != nil {
return err
}
}
// if traefikConfiguration.Configuration.ACME != nil {
// account := &acme.Account{}
//
// accountInitialized, err := keyExists(kv, traefikConfiguration.Configuration.ACME.Storage)
// if err != nil && err != store.ErrKeyNotFound {
// return err
// }
//
// // Check to see if ACME account object is already in kv store
// if traefikConfiguration.Configuration.ACME.OverrideCertificates || !accountInitialized {
//
// // Stores the ACME Account into the KV Store
// // Certificates in KV Stores will be overridden
// meta := cluster.NewMetadata(account)
// err = meta.Marshall()
// if err != nil {
// return err
// }
//
// source := staert.KvSource{
// Store: kv,
// Prefix: traefikConfiguration.Configuration.ACME.Storage,
// }
//
// err = source.StoreConfig(meta)
// if err != nil {
// return err
// }
// }
// }
return nil
}
}
// func keyExists(source *staert.KvSource, key string) (bool, error) {
// list, err := source.List(key, nil)
// if err != nil {
// return false, err
// }
//
// return len(list) > 0, nil
// }
// CreateKvSource creates KvSource
// TLS support is enable for Consul and Etcd backends
func CreateKvSource(traefikConfiguration *cmd.TraefikConfiguration) (*staert.KvSource, error) {
var kv *staert.KvSource
// var kvStore store.Store
var err error
// TODO kv store
// switch {
// case traefikConfiguration.Providers.Consul != nil:
// kvStore, err = traefikConfiguration.Providers.Consul.CreateStore()
// kv = &staert.KvSource{
// Store: kvStore,
// Prefix: traefikConfiguration.Providers.Consul.Prefix,
// }
// case traefikConfiguration.Providers.Etcd != nil:
// kvStore, err = traefikConfiguration.Providers.Etcd.CreateStore()
// kv = &staert.KvSource{
// Store: kvStore,
// Prefix: traefikConfiguration.Providers.Etcd.Prefix,
// }
// case traefikConfiguration.Providers.Zookeeper != nil:
// kvStore, err = traefikConfiguration.Providers.Zookeeper.CreateStore()
// kv = &staert.KvSource{
// Store: kvStore,
// Prefix: traefikConfiguration.Providers.Zookeeper.Prefix,
// }
// case traefikConfiguration.Providers.Boltdb != nil:
// kvStore, err = traefikConfiguration.Providers.Boltdb.CreateStore()
// kv = &staert.KvSource{
// Store: kvStore,
// Prefix: traefikConfiguration.Providers.Boltdb.Prefix,
// }
// }
return kv, err
}

View file

@ -4,38 +4,30 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
fmtlog "log" stdlog "log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"time" "time"
"github.com/cenkalti/backoff"
"github.com/containous/flaeg"
"github.com/containous/staert"
"github.com/containous/traefik/autogen/genstatic" "github.com/containous/traefik/autogen/genstatic"
"github.com/containous/traefik/cmd" "github.com/containous/traefik/cmd"
"github.com/containous/traefik/cmd/healthcheck" "github.com/containous/traefik/cmd/healthcheck"
"github.com/containous/traefik/cmd/storeconfig"
cmdVersion "github.com/containous/traefik/cmd/version" cmdVersion "github.com/containous/traefik/cmd/version"
"github.com/containous/traefik/pkg/cli"
"github.com/containous/traefik/pkg/collector" "github.com/containous/traefik/pkg/collector"
"github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/job"
"github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/provider/aggregator" "github.com/containous/traefik/pkg/provider/aggregator"
"github.com/containous/traefik/pkg/provider/kubernetes/k8s"
"github.com/containous/traefik/pkg/safe" "github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/server" "github.com/containous/traefik/pkg/server"
"github.com/containous/traefik/pkg/server/router" "github.com/containous/traefik/pkg/server/router"
traefiktls "github.com/containous/traefik/pkg/tls" traefiktls "github.com/containous/traefik/pkg/tls"
"github.com/containous/traefik/pkg/types"
"github.com/containous/traefik/pkg/version" "github.com/containous/traefik/pkg/version"
"github.com/coreos/go-systemd/daemon" "github.com/coreos/go-systemd/daemon"
assetfs "github.com/elazarl/go-bindata-assetfs" assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/ogier/pflag"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/vulcand/oxy/roundrobin" "github.com/vulcand/oxy/roundrobin"
) )
@ -48,141 +40,38 @@ func init() {
os.Setenv("GODEBUG", goDebug+"tls13=1") os.Setenv("GODEBUG", goDebug+"tls13=1")
} }
// sliceOfStrings is the parser for []string
type sliceOfStrings []string
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (s *sliceOfStrings) String() string {
return strings.Join(*s, ",")
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (s *sliceOfStrings) Set(value string) error {
parts := strings.Split(value, ",")
if len(parts) == 0 {
return fmt.Errorf("bad []string format: %s", value)
}
for _, entrypoint := range parts {
*s = append(*s, entrypoint)
}
return nil
}
// Get return the []string
func (s *sliceOfStrings) Get() interface{} {
return *s
}
// SetValue sets the []string with val
func (s *sliceOfStrings) SetValue(val interface{}) {
*s = val.([]string)
}
// Type is type of the struct
func (s *sliceOfStrings) Type() string {
return "sliceOfStrings"
}
func main() { func main() {
// traefik config inits // traefik config inits
traefikConfiguration := cmd.NewTraefikConfiguration() tConfig := cmd.NewTraefikConfiguration()
traefikPointersConfiguration := cmd.NewTraefikDefaultPointersConfiguration()
// traefik Command init loaders := []cli.ResourceLoader{&cli.FileLoader{}, &cli.EnvLoader{}, &cli.FlagLoader{}}
traefikCmd := &flaeg.Command{
cmdTraefik := &cli.Command{
Name: "traefik", Name: "traefik",
Description: `Traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. Description: `Traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
Complete documentation is available at https://traefik.io`, Complete documentation is available at https://traefik.io`,
Config: traefikConfiguration, Configuration: tConfig,
DefaultPointersConfig: traefikPointersConfiguration, Resources: loaders,
Run: func() error { Run: func(_ []string) error {
return runCmd(&traefikConfiguration.Configuration, traefikConfiguration.ConfigFile) return runCmd(&tConfig.Configuration, cli.GetConfigFile(loaders))
}, },
} }
// storeconfig Command init err := cmdTraefik.AddCommand(healthcheck.NewCmd(&tConfig.Configuration, loaders))
storeConfigCmd := storeconfig.NewCmd(traefikConfiguration, traefikPointersConfiguration)
// init flaeg source
f := flaeg.New(traefikCmd, os.Args[1:])
// add custom parsers
f.AddParser(reflect.TypeOf(static.EntryPoints{}), &static.EntryPoints{})
f.AddParser(reflect.SliceOf(reflect.TypeOf("")), &sliceOfStrings{})
f.AddParser(reflect.TypeOf(traefiktls.FilesOrContents{}), &traefiktls.FilesOrContents{})
f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{})
f.AddParser(reflect.TypeOf(k8s.Namespaces{}), &k8s.Namespaces{})
f.AddParser(reflect.TypeOf([]types.Domain{}), &types.Domains{})
f.AddParser(reflect.TypeOf(types.DNSResolvers{}), &types.DNSResolvers{})
f.AddParser(reflect.TypeOf(types.Buckets{}), &types.Buckets{})
f.AddParser(reflect.TypeOf(types.StatusCodes{}), &types.StatusCodes{})
f.AddParser(reflect.TypeOf(types.FieldNames{}), &types.FieldNames{})
f.AddParser(reflect.TypeOf(types.FieldHeaderNames{}), &types.FieldHeaderNames{})
// add commands
f.AddCommand(cmdVersion.NewCmd())
f.AddCommand(storeConfigCmd)
f.AddCommand(healthcheck.NewCmd(traefikConfiguration, traefikPointersConfiguration))
usedCmd, err := f.GetCommand()
if err != nil { if err != nil {
fmtlog.Println(err) stdlog.Println(err)
os.Exit(1) os.Exit(1)
} }
if _, err := f.Parse(usedCmd); err != nil { err = cmdTraefik.AddCommand(cmdVersion.NewCmd())
if err == pflag.ErrHelp {
os.Exit(0)
}
fmtlog.Printf("Error parsing command: %s\n", err)
os.Exit(1)
}
// staert init
s := staert.NewStaert(traefikCmd)
// init TOML source
toml := staert.NewTomlSource("traefik", []string{traefikConfiguration.ConfigFile, "/etc/traefik/", "$HOME/.traefik/", "."})
// add sources to staert
s.AddSource(toml)
s.AddSource(f)
if _, err := s.LoadConfig(); err != nil {
fmtlog.Printf("Error reading TOML config file %s : %s\n", toml.ConfigFileUsed(), err)
os.Exit(1)
}
traefikConfiguration.ConfigFile = toml.ConfigFileUsed()
kv, err := storeconfig.CreateKvSource(traefikConfiguration)
if err != nil { if err != nil {
fmtlog.Printf("Error creating kv store: %s\n", err) stdlog.Println(err)
os.Exit(1) os.Exit(1)
} }
storeConfigCmd.Run = storeconfig.Run(kv, traefikConfiguration)
// if a KV Store is enable and no sub-command called in args err = cli.Execute(cmdTraefik)
if kv != nil && usedCmd == traefikCmd {
s.AddSource(kv)
operation := func() error {
_, err := s.LoadConfig()
return err
}
notify := func(err error, time time.Duration) {
log.WithoutContext().Errorf("Load config error: %+v, retrying in %s", err, time)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
if err != nil { if err != nil {
fmtlog.Printf("Error loading configuration: %s\n", err) stdlog.Println(err)
os.Exit(1)
}
}
if err := s.Run(); err != nil {
fmtlog.Printf("Error running traefik: %s\n", err)
os.Exit(1) os.Exit(1)
} }
@ -192,10 +81,6 @@ Complete documentation is available at https://traefik.io`,
func runCmd(staticConfiguration *static.Configuration, configFile string) error { func runCmd(staticConfiguration *static.Configuration, configFile string) error {
configureLogging(staticConfiguration) configureLogging(staticConfiguration)
if len(configFile) > 0 {
log.WithoutContext().Infof("Using TOML configuration file %s", configFile)
}
http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
if err := roundrobin.SetDefaultWeight(0); err != nil { if err := roundrobin.SetDefaultWeight(0); err != nil {
@ -289,7 +174,11 @@ func runCmd(staticConfiguration *static.Configuration, configFile string) error
safe.Go(func() { safe.Go(func() {
tick := time.Tick(t) tick := time.Tick(t)
for range tick { for range tick {
_, errHealthCheck := healthcheck.Do(*staticConfiguration) resp, errHealthCheck := healthcheck.Do(*staticConfiguration)
if resp != nil {
resp.Body.Close()
}
if staticConfiguration.Ping == nil || errHealthCheck == nil { if staticConfiguration.Ping == nil || errHealthCheck == nil {
if ok, _ := daemon.SdNotify(false, "WATCHDOG=1"); !ok { if ok, _ := daemon.SdNotify(false, "WATCHDOG=1"); !ok {
log.WithoutContext().Error("Fail to tick watchdog") log.WithoutContext().Error("Fail to tick watchdog")
@ -309,7 +198,7 @@ func runCmd(staticConfiguration *static.Configuration, configFile string) error
func configureLogging(staticConfiguration *static.Configuration) { func configureLogging(staticConfiguration *static.Configuration) {
// configure default log flags // configure default log flags
fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags) stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
// configure log level // configure log level
// an explicitly defined log level always has precedence. if none is // an explicitly defined log level always has precedence. if none is

View file

@ -7,7 +7,7 @@ import (
"runtime" "runtime"
"text/template" "text/template"
"github.com/containous/flaeg" "github.com/containous/traefik/pkg/cli"
"github.com/containous/traefik/pkg/version" "github.com/containous/traefik/pkg/version"
) )
@ -18,19 +18,17 @@ Built: {{.BuildTime}}
OS/Arch: {{.Os}}/{{.Arch}}` OS/Arch: {{.Os}}/{{.Arch}}`
// NewCmd builds a new Version command // NewCmd builds a new Version command
func NewCmd() *flaeg.Command { func NewCmd() *cli.Command {
return &flaeg.Command{ return &cli.Command{
Name: "version", Name: "version",
Description: `Print version`, Description: `Shows the current Traefik version.`,
Config: struct{}{}, Configuration: nil,
DefaultPointersConfig: struct{}{}, Run: func(_ []string) error {
Run: func() error {
if err := GetPrint(os.Stdout); err != nil { if err := GetPrint(os.Stdout); err != nil {
return err return err
} }
fmt.Print("\n") fmt.Print("\n")
return nil return nil
}, },
} }
} }

View file

@ -33,31 +33,23 @@ Traefik gets its _dynamic configuration_ from [providers](../providers/overview.
## The Static Configuration ## The Static Configuration
There are three different locations where you can define static configuration options in Traefik: There are three different, mutually exclusive, ways to define static configuration options in Traefik:
- In a key-value store
- In the command-line arguments
- In a configuration file - In a configuration file
- As environment variables
- In the command-line arguments
If you don't provide a value for a given option, default values apply. These ways are evaluated in the order listed above.
!!! important "Precedence Order" If no value was provided for a given option, a default value applies.
Moreover, if an option has sub-options, and any of these sub-options is not specified, a default value will apply as well.
The following precedence order applies for configuration options: key-value > command-line > configuration file. For example, the `--providers.docker` option is enough by itself to enable the docker provider, even though sub-options like `--providers.docker.endpoint` exist.
Once positioned, this option sets (and resets) all the default values of the sub-options of `--providers.docker`.
It means that arguments override configuration file, and key-value store overrides arguments.
!!! important "Default Values"
Some root options are enablers: they set default values for all their children.
For example, the `--providers.docker` option enables the docker provider.
Once positioned, this option sets (and resets) all the default values under the root `providers.docker`.
If you define child options using a lesser precedence configuration source, they will be overwritten by the default values.
### Configuration File ### Configuration File
At startup, Traefik searches for a file named `traefik.toml` in `/etc/traefik/`, `$HOME/.traefik/`, and `.` (_the working directory_). At startup, Traefik searches for a file named `traefik.toml` in `/etc/traefik/`, `$XDG_CONFIG_HOME/`, `$HOME/.config/`, and `.` (_the working directory_).
You can override this using the `configFile` argument. You can override this using the `configFile` argument.
@ -67,16 +59,16 @@ traefik --configFile=foo/bar/myconfigfile.toml
### Arguments ### Arguments
Use `traefik --help` to get the list of the available arguments. To get the list of all available arguments:
### Key-Value Stores ```bash
traefik --help
Traefik supports several Key-value stores: # or
- [Consul](https://consul.io) docker run traefik[:version] --help
- [etcd](https://coreos.com/etcd/) # ex: docker run traefik:2.0 --help
- [ZooKeeper](https://zookeeper.apache.org/) ```
- [boltdb](https://github.com/boltdb/bolt)
## Available Configuration Options ## Available Configuration Options

View file

@ -6,48 +6,56 @@ The Traefik Command Line
## General ## General
```bash ```bash
traefik [command] [--flag=flag_argument] traefik [command] [flags] [arguments]
``` ```
Available commands: Use `traefik [command] --help` for help on any command.
- `version` : Print version Commands:
- `storeconfig` : Store the static Traefik configuration into a Key-value store. Please refer to the `Store Traefik configuration`(TODO: add doc and link) section to get documentation on it.
- `healthcheck`: Calls Traefik `/ping` to check health.
Each command can have additional flags. - `healthcheck` Calls Traefik `/ping` to check the health of Traefik (the API must be enabled).
- `version` Shows the current Traefik version.
All those flags will be displayed with: Flag's usage:
```bash ```bash
traefik [command] --help # set flag_argument to flag(s)
traefik [--flag=flag_argument] [-f [flag_argument]]
# set true/false to boolean flag(s)
traefik [--flag[=true|false| ]] [-f [true|false| ]]
``` ```
Each command is described at the beginning of the help section: ### healthcheck
```bash Calls Traefik `/ping` to check the health of Traefik.
traefik --help Its exit status is `0` if Traefik is healthy and `1` otherwise.
# or This can be used with Docker [HEALTHCHECK](https://docs.docker.com/engine/reference/builder/#healthcheck) instruction
or any other health check orchestration mechanism.
docker run traefik[:version] --help
# ex: docker run traefik:1.5 --help
```
### Command: healthcheck
Checks the health of Traefik.
Its exit status is `0` if Traefik is healthy and `1` if it is unhealthy.
This can be used with Docker [HEALTHCHECK](https://docs.docker.com/engine/reference/builder/#healthcheck) instruction or any other health check orchestration mechanism.
!!! note !!! note
The [`ping` endpoint](../ping/) must be enabled to allow the `healthcheck` command to call `/ping`. The [`ping` endpoint](../ping/) must be enabled to allow the `healthcheck` command to call `/ping`.
```bash Usage:
traefik healthcheck
```
```bash ```bash
traefik healthcheck [command] [flags] [arguments]
```
Example:
```bash
$ traefik healthcheck
OK: http://:8082/ping OK: http://:8082/ping
``` ```
### version
Shows the current Traefik version.
Usage:
```bash
traefik version [command] [flags] [arguments]
```

View file

@ -63,7 +63,7 @@ Attach labels to your containers and let Traefik do the rest!
## Provider Configuration Options ## Provider Configuration Options
!!! tip "Browse the Reference" !!! tip "Browse the Reference"
If you're in a hurry, maybe you'd rather go through the [static](../reference/static-configuration.md) and the [dynamic](../reference/dynamic-configuration/docker.md) configuration references. If you're in a hurry, maybe you'd rather go through the [static](../reference/static-configuration/overview.md) and the [dynamic](../reference/dynamic-configuration/docker.md) configuration references.
### `endpoint` ### `endpoint`

View file

@ -53,7 +53,7 @@ You can write these configuration elements:
## Provider Configuration Options ## Provider Configuration Options
!!! tip "Browse the Reference" !!! tip "Browse the Reference"
If you're in a hurry, maybe you'd rather go through the [static](../reference/static-configuration.md) and the [dynamic](../reference/dynamic-configuration/file.md) configuration references. If you're in a hurry, maybe you'd rather go through the [static](../reference/static-configuration/overview.md) and the [dynamic](../reference/dynamic-configuration/file.md) configuration references.
### `filename` ### `filename`

View file

@ -49,7 +49,7 @@ See also [Marathon user guide](../user-guides/marathon.md).
## Provider Configuration Options ## Provider Configuration Options
!!! tip "Browse the Reference" !!! tip "Browse the Reference"
If you're in a hurry, maybe you'd rather go through the [static](../reference/static-configuration.md) and the [dynamic](../reference/dynamic-configuration/marathon.md) configuration references. If you're in a hurry, maybe you'd rather go through the [static](../reference/static-configuration/overview.md) and the [dynamic](../reference/dynamic-configuration/marathon.md) configuration references.
### `basic` ### `basic`

View file

@ -1,13 +0,0 @@
# Static Configuration
## File
```toml
--8<-- "content/reference/static-configuration.toml"
```
## CLI
```txt
--8<-- "content/reference/static-configuration.txt"
```

View file

@ -1,202 +0,0 @@
--accesslog Access log settings (default "false")
--accesslog.bufferingsize Number of access log lines to process in a buffered way. Default 0. (default "0")
--accesslog.fields AccessLogFields (default "false")
--accesslog.fields.defaultmode Default mode for fields: keep | drop (default "keep")
--accesslog.fields.headers Headers to keep, drop or redact (default "false")
--accesslog.fields.headers.defaultmode Default mode for fields: keep | drop | redact (default "keep")
--accesslog.fields.headers.names Override mode for headers (default "map[]")
--accesslog.fields.names Override mode for fields (default "map[]")
--accesslog.filepath Access log file path. Stdout is used when omitted or empty
--accesslog.filters Access log filters, used to keep only specific access logs (default "false")
--accesslog.filters.minduration Keep access logs when request took longer than the specified duration (default "0s")
--accesslog.filters.retryattempts Keep access logs when at least one retry happened (default "false")
--accesslog.filters.statuscodes Keep access logs with status codes in the specified range (default "[]")
--accesslog.format Access log format: json | common (default "common")
--acme Enable ACME (Let's Encrypt): automatic SSL (default "false")
--acme.acmelogging Enable debug logging of ACME actions. (default "false")
--acme.caserver CA server to use.
--acme.dnschallenge Activate DNS-01 Challenge (default "false")
--acme.dnschallenge.delaybeforecheck Assume DNS propagates after a delay in seconds rather than finding and querying (default "0s")
nameservers.
--acme.dnschallenge.disablepropagationcheck Disable the DNS propagation checks before notifying ACME that the DNS challenge (default "false")
is ready. [not recommended]
--acme.dnschallenge.provider Use a DNS-01 based challenge provider rather than HTTPS.
--acme.dnschallenge.resolvers Use following DNS servers to resolve the FQDN authority.
--acme.domains CN and SANs (alternative domains) to each main domain using format: (default "[]")
--acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. No
SANs for wildcards domain. Wildcard domains only accepted with DNSChallenge
--acme.email Email address used for registration
--acme.entrypoint EntryPoint to use.
--acme.httpchallenge Activate HTTP-01 Challenge (default "false")
--acme.httpchallenge.entrypoint HTTP challenge EntryPoint
--acme.keytype KeyType used for generating certificate private key. Allow value 'EC256',
'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'
--acme.onhostrule Enable certificate generation on frontends Host rules. (default "false")
--acme.storage Storage to use.
--acme.tlschallenge Activate TLS-ALPN-01 Challenge (default "false")
--api Enable api/dashboard (default "false")
--api.dashboard Activate dashboard (default "true")
--api.entrypoint EntryPoint (default "traefik")
--api.middlewares Middleware list
--api.statistics Enable more detailed statistics (default "true")
--api.statistics.recenterrors Number of recent errors logged (default "10")
-c, --configfile Configuration file to use (TOML).
--entrypoints Entrypoints definition using format: --entryPoints='Name:http Address::8000' (default "map[]")
--entryPoints='Name:https Address::4442'
--global Global configuration options (default "true")
--global.checknewversion Periodically check if a new version has been released (default "true")
-d, --global.debug Enable debug mode (default "false")
--global.sendanonymoususage send periodically anonymous usage statistics (default "false")
--hostresolver Enable CNAME Flattening (default "false")
--hostresolver.cnameflattening A flag to enable/disable CNAME flattening (default "false")
--hostresolver.resolvconfig resolv.conf used for DNS resolving (default "/etc/resolv.conf")
--hostresolver.resolvdepth The maximal depth of DNS recursive resolving (default "5")
--log Traefik log settings (default "false")
--log.filepath Traefik log file path. Stdout is used when omitted or empty
--log.format Traefik log format: json | common (default "common")
--log.level Log level set to traefik logs.
--metrics Enable a metrics exporter (default "false")
--metrics.datadog DataDog metrics exporter type (default "false")
--metrics.datadog.address DataDog's address (default "localhost:8125")
--metrics.datadog.pushinterval DataDog push interval (default "10s")
--metrics.influxdb InfluxDB metrics exporter type (default "false")
--metrics.influxdb.address InfluxDB address (default "localhost:8089")
--metrics.influxdb.database InfluxDB database used when protocol is http
--metrics.influxdb.password InfluxDB password (only with http)
--metrics.influxdb.protocol InfluxDB address protocol (udp or http) (default "udp")
--metrics.influxdb.pushinterval InfluxDB push interval (default "10s")
--metrics.influxdb.retentionpolicy InfluxDB retention policy used when protocol is http
--metrics.influxdb.username InfluxDB username (only with http)
--metrics.prometheus Prometheus metrics exporter type (default "false")
--metrics.prometheus.buckets Buckets for latency metrics (default "[0.1 0.3 1.2 5]")
--metrics.prometheus.entrypoint EntryPoint (default "traefik")
--metrics.prometheus.middlewares Middlewares
--metrics.statsd StatsD metrics exporter type (default "false")
--metrics.statsd.address StatsD address (default "localhost:8125")
--metrics.statsd.pushinterval StatsD push interval (default "10s")
--ping Enable ping (default "false")
--ping.entrypoint Ping entryPoint (default "traefik")
--ping.middlewares Middleware list
--providers Providers configuration (default "false")
--providers.docker Enable Docker backend with default settings (default "false")
--providers.docker.constraints Filter services by constraint, matching with Traefik tags. (default "[]")
--providers.docker.defaultrule Default rule (default "Host(`{{ normalize .Name }}`)")
--providers.docker.endpoint Docker server endpoint. Can be a tcp or a unix socket endpoint (default "unix:///var/run/docker.sock")
--providers.docker.exposedbydefault Expose containers by default (default "true")
--providers.docker.network Default Docker network used
--providers.docker.swarmmode Use Docker on Swarm Mode (default "false")
--providers.docker.swarmmoderefreshseconds Polling interval for swarm mode (in seconds) (default "15")
--providers.docker.tls Enable Docker TLS support (default "false")
--providers.docker.tls.ca TLS CA
--providers.docker.tls.caoptional TLS CA.Optional (default "false")
--providers.docker.tls.cert TLS cert
--providers.docker.tls.insecureskipverify TLS insecure skip verify (default "false")
--providers.docker.tls.key TLS key
--providers.docker.usebindportip Use the ip address from the bound port, rather than from the inner network (default "false")
--providers.docker.watch Watch provider (default "true")
--providers.file Enable File backend with default settings (default "true")
--providers.file.debugloggeneratedtemplate Enable debug logging of generated configuration template. (default "false")
--providers.file.directory Load configuration from one or more .toml files in a directory
--providers.file.filename Override default configuration template. For advanced users :)
--providers.file.watch Watch provider (default "true")
--providers.kubernetes Enable Kubernetes backend with default settings (default "true")
--providers.kubernetes.certauthfilepath Kubernetes certificate authority file path (not needed for in-cluster client)
--providers.kubernetes.disablepasshostheaders Kubernetes disable PassHost Headers (default "false")
--providers.kubernetes.endpoint Kubernetes server endpoint (required for external cluster client)
--providers.kubernetes.ingressclass Value of kubernetes.io/ingress.class annotation to watch for
--providers.kubernetes.ingressendpoint Kubernetes Ingress Endpoint (default "false")
--providers.kubernetes.ingressendpoint.hostname Hostname used for Kubernetes Ingress endpoints
--providers.kubernetes.ingressendpoint.ip IP used for Kubernetes Ingress endpoints
--providers.kubernetes.ingressendpoint.publishedservice Published Kubernetes Service to copy status from
--providers.kubernetes.labelselector Kubernetes Ingress label selector to use
--providers.kubernetes.namespaces Kubernetes namespaces (default "[]")
--providers.kubernetes.token Kubernetes bearer token (not needed for in-cluster client)
--providers.kubernetescrd Enable Kubernetes backend with default settings (default "false")
--providers.kubernetescrd.certauthfilepath Kubernetes certificate authority file path (not needed for in-cluster client)
--providers.kubernetescrd.disablepasshostheaders Kubernetes disable PassHost Headers (default "false")
--providers.kubernetescrd.endpoint Kubernetes server endpoint (required for external cluster client)
--providers.kubernetescrd.ingressclass Value of kubernetes.io/ingress.class annotation to watch for
--providers.kubernetescrd.labelselector Kubernetes label selector to use
--providers.kubernetescrd.namespaces Kubernetes namespaces (default "[]")
--providers.kubernetescrd.token Kubernetes bearer token (not needed for in-cluster client)
--providers.marathon Enable Marathon backend with default settings (default "false")
--providers.marathon.basic Enable basic authentication (default "false")
--providers.marathon.basic.httpbasicauthuser Basic authentication User
--providers.marathon.basic.httpbasicpassword Basic authentication Password
--providers.marathon.constraints Filter services by constraint, matching with Traefik tags. (default "[]")
--providers.marathon.dcostoken DCOSToken for DCOS environment, This will override the Authorization header
--providers.marathon.defaultrule Default rule (default "Host(`{{ normalize .Name }}`)")
--providers.marathon.dialertimeout Set a dialer timeout for Marathon (default "5s")
--providers.marathon.endpoint Marathon server endpoint. You can also specify multiple endpoint for Marathon (default "http://127.0.0.1:8080")
--providers.marathon.exposedbydefault Expose Marathon apps by default (default "true")
--providers.marathon.filtermarathonconstraints Enable use of Marathon constraints in constraint filtering (default "false")
--providers.marathon.forcetaskhostname Force to use the task's hostname. (default "false")
--providers.marathon.keepalive Set a TCP Keep Alive time in seconds (default "10s")
--providers.marathon.respectreadinesschecks Filter out tasks with non-successful readiness checks during deployments (default "false")
--providers.marathon.responseheadertimeout Set a response header timeout for Marathon (default "1m0s")
--providers.marathon.tls Enable TLS support (default "false")
--providers.marathon.tls.ca TLS CA
--providers.marathon.tls.caoptional TLS CA.Optional (default "false")
--providers.marathon.tls.cert TLS cert
--providers.marathon.tls.insecureskipverify TLS insecure skip verify (default "false")
--providers.marathon.tls.key TLS key
--providers.marathon.tlshandshaketimeout Set a TLS handhsake timeout for Marathon (default "5s")
--providers.marathon.trace Display additional provider logs. (default "false")
--providers.marathon.watch Watch provider (default "true")
--providers.providersthrottleduration Backends throttle duration: minimum duration between 2 events from providers (default "2s")
before applying a new configuration. It avoids unnecessary reloads if multiples
events are sent in a short amount of time.
--providers.rancher Enable Rancher backend with default settings (default "true")
--providers.rancher.constraints Filter services by constraint, matching with Traefik tags. (default "[]")
--providers.rancher.defaultrule Default rule (default "Host(`{{ normalize .Name }}`)")
--providers.rancher.exposedbydefault Expose containers by default (default "true")
--providers.rancher.intervalpoll Poll the Rancher metadata service every 'rancher.refreshseconds' (less accurate) (default "false")
--providers.rancher.prefix Prefix used for accessing the Rancher metadata service (default "latest")
--providers.rancher.watch Watch provider (default "true")
--providers.rest Enable Rest backend with default settings (default "true")
--providers.rest.entrypoint EntryPoint (default "traefik")
--serverstransport Servers default transport (default "true")
--serverstransport.forwardingtimeouts Timeouts for requests forwarded to the backend servers (default "true")
--serverstransport.forwardingtimeouts.dialtimeout The amount of time to wait until a connection to a backend server can be (default "0s")
established. Defaults to 30 seconds. If zero, no timeout exists
--serverstransport.forwardingtimeouts.responseheadertimeout The amount of time to wait for a server's response headers after fully writing (default "0s")
the request (including its body, if any). If zero, no timeout exists
--serverstransport.insecureskipverify Disable SSL certificate verification (default "false")
--serverstransport.maxidleconnsperhost If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, (default "200")
DefaultMaxIdleConnsPerHost is used
--serverstransport.rootcas Add cert file for self-signed certificate
--tracing OpenTracing configuration (default "false")
--tracing.backend Selects the tracking backend ('jaeger','zipkin','datadog','instana'). (default "jaeger")
--tracing.datadog Settings for DataDog (default "false")
--tracing.datadog.bagageprefixheadername specifies the header name prefix that will be used to store baggage items in a
map.
--tracing.datadog.debug Enable DataDog debug. (default "false")
--tracing.datadog.globaltag Key:Value tag to be set on all the spans.
--tracing.datadog.localagenthostport Set datadog-agent's host:port that the reporter will used. Defaults to (default "localhost:8126")
localhost:8126
--tracing.datadog.parentidheadername Specifies the header name that will be used to store the parent ID.
--tracing.datadog.prioritysampling Enable priority sampling. When using distributed tracing, this option must be (default "false")
enabled in order to get all the parts of a distributed trace sampled.
--tracing.datadog.samplingpriorityheadername Specifies the header name that will be used to store the sampling priority.
--tracing.datadog.traceidheadername Specifies the header name that will be used to store the trace ID.
--tracing.instana Settings for Instana (default "false")
--tracing.instana.localagenthost Set instana-agent's host that the reporter will used. (default "localhost")
--tracing.instana.localagentport Set instana-agent's port that the reporter will used. (default "42699")
--tracing.instana.loglevel Set instana-agent's log level. ('error','warn','info','debug') (default "info")
--tracing.jaeger Settings for jaeger (default "false")
--tracing.jaeger.gen128bit generate 128 bit span IDs. (default "false")
--tracing.jaeger.localagenthostport set jaeger-agent's host:port that the reporter will used. (default "127.0.0.1:6831")
--tracing.jaeger.propagation which propgation format to use (jaeger/b3). (default "jaeger")
--tracing.jaeger.samplingparam set the sampling parameter. (default "1")
--tracing.jaeger.samplingserverurl set the sampling server url. (default "http://localhost:5778/sampling")
--tracing.jaeger.samplingtype set the sampling type. (default "const")
--tracing.jaeger.tracecontextheadername set the header to use for the trace-id. (default "uber-trace-id")
--tracing.servicename Set the name for this service (default "traefik")
--tracing.spannamelimit Set the maximum character limit for Span names (default 0 = no limit) (default "0")
--tracing.zipkin Settings for zipkin (default "false")
--tracing.zipkin.debug Enable Zipkin debug. (default "false")
--tracing.zipkin.httpendpoint HTTP Endpoint to report traces to. (default "http://localhost:9411/api/v1/spans")
--tracing.zipkin.id128bit Use Zipkin 128 bit root span IDs. (default "true")
--tracing.zipkin.samespan Use Zipkin SameSpan RPC style traces. (default "false")
--tracing.zipkin.samplerate The rate between 0.0 and 1.0 of requests to trace. (default "1")
-h, --help Print Help (this message) and exit

View file

@ -0,0 +1,5 @@
# Static Configuration: CLI
```txt
--8<-- "content/reference/static-configuration/cli.txt"
```

View file

@ -0,0 +1,635 @@
--accesslog (Default: "false")
Access log settings.
--accesslog.bufferingsize (Default: "0")
Number of access log lines to process in a buffered way.
--accesslog.fields.defaultmode (Default: "keep")
Default mode for fields: keep | drop
--accesslog.fields.headers.defaultmode (Default: "keep")
Default mode for fields: keep | drop | redact
--accesslog.fields.headers.names.<name> (Default: "")
Override mode for headers
--accesslog.fields.names.<name> (Default: "")
Override mode for fields
--accesslog.filepath (Default: "")
Access log file path. Stdout is used when omitted or empty.
--accesslog.filters.minduration (Default: "0")
Keep access logs when request took longer than the specified duration.
--accesslog.filters.retryattempts (Default: "false")
Keep access logs when at least one retry happened.
--accesslog.filters.statuscodes (Default: "")
Keep access logs with status codes in the specified range.
--accesslog.format (Default: "common")
Access log format: json | common
--acme.acmelogging (Default: "false")
Enable debug logging of ACME actions.
--acme.caserver (Default: "https://acme-v02.api.letsencrypt.org/directory")
CA server to use.
--acme.dnschallenge (Default: "false")
Activate DNS-01 Challenge.
--acme.dnschallenge.delaybeforecheck (Default: "0")
Assume DNS propagates after a delay in seconds rather than finding and querying
nameservers.
--acme.dnschallenge.disablepropagationcheck (Default: "false")
Disable the DNS propagation checks before notifying ACME that the DNS challenge
is ready. [not recommended]
--acme.dnschallenge.provider (Default: "")
Use a DNS-01 based challenge provider rather than HTTPS.
--acme.dnschallenge.resolvers (Default: "")
Use following DNS servers to resolve the FQDN authority.
--acme.domains (Default: "")
The list of domains for which certificates are generated on startup. Wildcard
domains only accepted with DNSChallenge.
--acme.domains[n].main (Default: "")
Default subject name.
--acme.domains[n].sans (Default: "")
Subject alternative names.
--acme.email (Default: "")
Email address used for registration.
--acme.entrypoint (Default: "")
EntryPoint to use.
--acme.httpchallenge (Default: "false")
Activate HTTP-01 Challenge.
--acme.httpchallenge.entrypoint (Default: "")
HTTP challenge EntryPoint
--acme.keytype (Default: "RSA4096")
KeyType used for generating certificate private key. Allow value 'EC256',
'EC384', 'RSA2048', 'RSA4096', 'RSA8192'.
--acme.onhostrule (Default: "false")
Enable certificate generation on router Host rules.
--acme.storage (Default: "acme.json")
Storage to use.
--acme.tlschallenge (Default: "true")
Activate TLS-ALPN-01 Challenge.
--api (Default: "false")
Enable api/dashboard.
--api.dashboard (Default: "true")
Activate dashboard.
--api.entrypoint (Default: "traefik")
EntryPoint.
--api.middlewares (Default: "")
Middleware list.
--api.statistics (Default: "false")
Enable more detailed statistics.
--api.statistics.recenterrors (Default: "10")
Number of recent errors logged.
--configfile (Default: "")
Configuration file to use. If specified all other flags are ignored.
--entrypoints.<name> (Default: "false")
Entry points definition.
--entrypoints.<name>.address (Default: "")
Entry point address.
--entrypoints.<name>.forwardedheaders.insecure (Default: "false")
Trust all forwarded headers.
--entrypoints.<name>.forwardedheaders.trustedips (Default: "")
Trust only forwarded headers from selected IPs.
--entrypoints.<name>.proxyprotocol (Default: "false")
Proxy-Protocol configuration.
--entrypoints.<name>.proxyprotocol.insecure (Default: "false")
Trust all.
--entrypoints.<name>.proxyprotocol.trustedips (Default: "")
Trust only selected IPs.
--entrypoints.<name>.transport.lifecycle.gracetimeout (Default: "10")
Duration to give active requests a chance to finish before Traefik stops.
--entrypoints.<name>.transport.lifecycle.requestacceptgracetimeout (Default: "0")
Duration to keep accepting requests before Traefik initiates the graceful
shutdown procedure.
--entrypoints.<name>.transport.respondingtimeouts.idletimeout (Default: "180")
IdleTimeout is the maximum amount duration an idle (keep-alive) connection will
remain idle before closing itself. If zero, no timeout is set.
--entrypoints.<name>.transport.respondingtimeouts.readtimeout (Default: "0")
ReadTimeout is the maximum duration for reading the entire request, including
the body. If zero, no timeout is set.
--entrypoints.<name>.transport.respondingtimeouts.writetimeout (Default: "0")
WriteTimeout is the maximum duration before timing out writes of the response.
If zero, no timeout is set.
--global.checknewversion (Default: "true")
Periodically check if a new version has been released.
--global.debug (Default: "false")
Enable debug mode.
--global.sendanonymoususage
Periodically send anonymous usage statistics. If the option is not specified, it
will be enabled by default.
--hostresolver (Default: "false")
Enable CNAME Flattening.
--hostresolver.cnameflattening (Default: "false")
A flag to enable/disable CNAME flattening
--hostresolver.resolvconfig (Default: "/etc/resolv.conf")
resolv.conf used for DNS resolving
--hostresolver.resolvdepth (Default: "5")
The maximal depth of DNS recursive resolving
--log.filepath (Default: "")
Traefik log file path. Stdout is used when omitted or empty.
--log.format (Default: "common")
Traefik log format: json | common
--log.level (Default: "ERROR")
Log level set to traefik logs.
--metrics.datadog (Default: "false")
DataDog metrics exporter type.
--metrics.datadog.address (Default: "localhost:8125")
DataDog's address.
--metrics.datadog.pushinterval (Default: "10")
DataDog push interval.
--metrics.influxdb (Default: "false")
InfluxDB metrics exporter type.
--metrics.influxdb.address (Default: "localhost:8089")
InfluxDB address.
--metrics.influxdb.database (Default: "")
InfluxDB database used when protocol is http.
--metrics.influxdb.password (Default: "")
InfluxDB password (only with http).
--metrics.influxdb.protocol (Default: "udp")
InfluxDB address protocol (udp or http).
--metrics.influxdb.pushinterval (Default: "10")
InfluxDB push interval.
--metrics.influxdb.retentionpolicy (Default: "")
InfluxDB retention policy used when protocol is http.
--metrics.influxdb.username (Default: "")
InfluxDB username (only with http).
--metrics.prometheus (Default: "false")
Prometheus metrics exporter type.
--metrics.prometheus.buckets (Default: "0.100000, 0.300000, 1.200000, 5.000000")
Buckets for latency metrics.
--metrics.prometheus.entrypoint (Default: "traefik")
EntryPoint.
--metrics.prometheus.middlewares (Default: "")
Middlewares.
--metrics.statsd (Default: "false")
StatsD metrics exporter type.
--metrics.statsd.address (Default: "localhost:8125")
StatsD address.
--metrics.statsd.pushinterval (Default: "10")
StatsD push interval.
--ping (Default: "false")
Enable ping.
--ping.entrypoint (Default: "traefik")
Ping entryPoint.
--ping.middlewares (Default: "")
Middleware list.
--providers.docker (Default: "false")
Enable Docker backend with default settings.
--providers.docker.constraints (Default: "")
Filter services by constraint, matching with Traefik tags.
--providers.docker.constraints[n].key (Default: "")
The provider label that will be matched against. In practice, it is always
'tag'.
--providers.docker.constraints[n].mustmatch (Default: "false")
Whether the matching operator is equals or not equals.
--providers.docker.constraints[n].value (Default: "")
The value that will be matched against.
--providers.docker.defaultrule (Default: "Host(`{{ normalize .Name }}`)")
Default rule.
--providers.docker.endpoint (Default: "unix:///var/run/docker.sock")
Docker server endpoint. Can be a tcp or a unix socket endpoint.
--providers.docker.exposedbydefault (Default: "true")
Expose containers by default.
--providers.docker.network (Default: "")
Default Docker network used.
--providers.docker.swarmmode (Default: "false")
Use Docker on Swarm Mode.
--providers.docker.swarmmoderefreshseconds (Default: "15")
Polling interval for swarm mode.
--providers.docker.tls.ca (Default: "")
TLS CA
--providers.docker.tls.caoptional (Default: "false")
TLS CA.Optional
--providers.docker.tls.cert (Default: "")
TLS cert
--providers.docker.tls.insecureskipverify (Default: "false")
TLS insecure skip verify
--providers.docker.tls.key (Default: "")
TLS key
--providers.docker.usebindportip (Default: "false")
Use the ip address from the bound port, rather than from the inner network.
--providers.docker.watch (Default: "true")
Watch provider.
--providers.file (Default: "false")
Enable File backend with default settings.
--providers.file.debugloggeneratedtemplate (Default: "false")
Enable debug logging of generated configuration template.
--providers.file.directory (Default: "")
Load configuration from one or more .toml files in a directory.
--providers.file.filename (Default: "")
Override default configuration template. For advanced users :)
--providers.file.watch (Default: "true")
Watch provider.
--providers.kubernetes (Default: "false")
Enable Kubernetes backend with default settings.
--providers.kubernetes.certauthfilepath (Default: "")
Kubernetes certificate authority file path (not needed for in-cluster client).
--providers.kubernetes.disablepasshostheaders (Default: "false")
Kubernetes disable PassHost Headers.
--providers.kubernetes.endpoint (Default: "")
Kubernetes server endpoint (required for external cluster client).
--providers.kubernetes.ingressclass (Default: "")
Value of kubernetes.io/ingress.class annotation to watch for.
--providers.kubernetes.ingressendpoint.hostname (Default: "")
Hostname used for Kubernetes Ingress endpoints.
--providers.kubernetes.ingressendpoint.ip (Default: "")
IP used for Kubernetes Ingress endpoints.
--providers.kubernetes.ingressendpoint.publishedservice (Default: "")
Published Kubernetes Service to copy status from.
--providers.kubernetes.labelselector (Default: "")
Kubernetes Ingress label selector to use.
--providers.kubernetes.namespaces (Default: "")
Kubernetes namespaces.
--providers.kubernetes.token (Default: "")
Kubernetes bearer token (not needed for in-cluster client).
--providers.kubernetescrd (Default: "false")
Enable Kubernetes backend with default settings.
--providers.kubernetescrd.certauthfilepath (Default: "")
Kubernetes certificate authority file path (not needed for in-cluster client).
--providers.kubernetescrd.disablepasshostheaders (Default: "false")
Kubernetes disable PassHost Headers.
--providers.kubernetescrd.endpoint (Default: "")
Kubernetes server endpoint (required for external cluster client).
--providers.kubernetescrd.ingressclass (Default: "")
Value of kubernetes.io/ingress.class annotation to watch for.
--providers.kubernetescrd.labelselector (Default: "")
Kubernetes label selector to use.
--providers.kubernetescrd.namespaces (Default: "")
Kubernetes namespaces.
--providers.kubernetescrd.token (Default: "")
Kubernetes bearer token (not needed for in-cluster client).
--providers.marathon (Default: "false")
Enable Marathon backend with default settings.
--providers.marathon.basic.httpbasicauthuser (Default: "")
Basic authentication User.
--providers.marathon.basic.httpbasicpassword (Default: "")
Basic authentication Password.
--providers.marathon.constraints (Default: "")
Filter services by constraint, matching with Traefik tags.
--providers.marathon.constraints[n].key (Default: "")
The provider label that will be matched against. In practice, it is always
'tag'.
--providers.marathon.constraints[n].mustmatch (Default: "false")
Whether the matching operator is equals or not equals.
--providers.marathon.constraints[n].value (Default: "")
The value that will be matched against.
--providers.marathon.dcostoken (Default: "")
DCOSToken for DCOS environment, This will override the Authorization header.
--providers.marathon.defaultrule (Default: "Host(`{{ normalize .Name }}`)")
Default rule.
--providers.marathon.dialertimeout (Default: "5")
Set a dialer timeout for Marathon.
--providers.marathon.endpoint (Default: "http://127.0.0.1:8080")
Marathon server endpoint. You can also specify multiple endpoint for Marathon.
--providers.marathon.exposedbydefault (Default: "true")
Expose Marathon apps by default.
--providers.marathon.filtermarathonconstraints (Default: "false")
Enable use of Marathon constraints in constraint filtering.
--providers.marathon.forcetaskhostname (Default: "false")
Force to use the task's hostname.
--providers.marathon.keepalive (Default: "10")
Set a TCP Keep Alive time.
--providers.marathon.respectreadinesschecks (Default: "false")
Filter out tasks with non-successful readiness checks during deployments.
--providers.marathon.responseheadertimeout (Default: "60")
Set a response header timeout for Marathon.
--providers.marathon.tls.ca (Default: "")
TLS CA
--providers.marathon.tls.caoptional (Default: "false")
TLS CA.Optional
--providers.marathon.tls.cert (Default: "")
TLS cert
--providers.marathon.tls.insecureskipverify (Default: "false")
TLS insecure skip verify
--providers.marathon.tls.key (Default: "")
TLS key
--providers.marathon.tlshandshaketimeout (Default: "5")
Set a TLS handshake timeout for Marathon.
--providers.marathon.trace (Default: "false")
Display additional provider logs.
--providers.marathon.watch (Default: "true")
Watch provider.
--providers.providersthrottleduration (Default: "2")
Backends throttle duration: minimum duration between 2 events from providers
before applying a new configuration. It avoids unnecessary reloads if multiples
events are sent in a short amount of time.
--providers.rancher (Default: "false")
Enable Rancher backend with default settings.
--providers.rancher.constraints (Default: "")
Filter services by constraint, matching with Traefik tags.
--providers.rancher.constraints[n].key (Default: "")
The provider label that will be matched against. In practice, it is always
'tag'.
--providers.rancher.constraints[n].mustmatch (Default: "false")
Whether the matching operator is equals or not equals.
--providers.rancher.constraints[n].value (Default: "")
The value that will be matched against.
--providers.rancher.defaultrule (Default: "Host(`{{ normalize .Name }}`)")
Default rule.
--providers.rancher.enableservicehealthfilter (Default: "true")
Filter services with unhealthy states and inactive states.
--providers.rancher.exposedbydefault (Default: "true")
Expose containers by default.
--providers.rancher.intervalpoll (Default: "false")
Poll the Rancher metadata service every 'rancher.refreshseconds' (less
accurate).
--providers.rancher.prefix (Default: "latest")
Prefix used for accessing the Rancher metadata service.
--providers.rancher.refreshseconds (Default: "15")
Defines the polling interval in seconds.
--providers.rancher.watch (Default: "true")
Watch provider.
--providers.rest (Default: "false")
Enable Rest backend with default settings.
--providers.rest.entrypoint (Default: "traefik")
EntryPoint.
--serverstransport.forwardingtimeouts.dialtimeout (Default: "30")
The amount of time to wait until a connection to a backend server can be
established. If zero, no timeout exists.
--serverstransport.forwardingtimeouts.responseheadertimeout (Default: "0")
The amount of time to wait for a server's response headers after fully writing
the request (including its body, if any). If zero, no timeout exists.
--serverstransport.insecureskipverify (Default: "false")
Disable SSL certificate verification.
--serverstransport.maxidleconnsperhost (Default: "200")
If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero,
DefaultMaxIdleConnsPerHost is used
--serverstransport.rootcas (Default: "")
Add cert file for self-signed certificate.
--tracing (Default: "false")
OpenTracing configuration.
--tracing.backend (Default: "jaeger")
Selects the tracking backend ('jaeger','zipkin','datadog','instana').
--tracing.datadog (Default: "false")
Settings for DataDog.
--tracing.datadog.bagageprefixheadername (Default: "")
Specifies the header name prefix that will be used to store baggage items in a
map.
--tracing.datadog.debug (Default: "false")
Enable DataDog debug.
--tracing.datadog.globaltag (Default: "")
Key:Value tag to be set on all the spans.
--tracing.datadog.localagenthostport (Default: "localhost:8126")
Set datadog-agent's host:port that the reporter will used.
--tracing.datadog.parentidheadername (Default: "")
Specifies the header name that will be used to store the parent ID.
--tracing.datadog.prioritysampling (Default: "false")
Enable priority sampling. When using distributed tracing, this option must be
enabled in order to get all the parts of a distributed trace sampled.
--tracing.datadog.samplingpriorityheadername (Default: "")
Specifies the header name that will be used to store the sampling priority.
--tracing.datadog.traceidheadername (Default: "")
Specifies the header name that will be used to store the trace ID.
--tracing.haystack (Default: "false")
Settings for Haystack.
--tracing.haystack.baggageprefixheadername (Default: "")
specifies the header name prefix that will be used to store baggage items in a
map.
--tracing.haystack.globaltag (Default: "")
Key:Value tag to be set on all the spans.
--tracing.haystack.localagenthost (Default: "LocalAgentHost")
Set haystack-agent's host that the reporter will used.
--tracing.haystack.localagentport (Default: "35000")
Set haystack-agent's port that the reporter will used.
--tracing.haystack.parentidheadername (Default: "")
Specifies the header name that will be used to store the parent ID.
--tracing.haystack.spanidheadername (Default: "")
Specifies the header name that will be used to store the span ID.
--tracing.haystack.traceidheadername (Default: "")
Specifies the header name that will be used to store the trace ID.
--tracing.instana (Default: "false")
Settings for Instana.
--tracing.instana.localagenthost (Default: "localhost")
Set instana-agent's host that the reporter will used.
--tracing.instana.localagentport (Default: "42699")
Set instana-agent's port that the reporter will used.
--tracing.instana.loglevel (Default: "info")
Set instana-agent's log level. ('error','warn','info','debug')
--tracing.jaeger (Default: "false")
Settings for jaeger.
--tracing.jaeger.gen128bit (Default: "false")
Generate 128 bit span IDs.
--tracing.jaeger.localagenthostport (Default: "127.0.0.1:6831")
Set jaeger-agent's host:port that the reporter will used.
--tracing.jaeger.propagation (Default: "jaeger")
Which propgation format to use (jaeger/b3).
--tracing.jaeger.samplingparam (Default: "1.000000")
Set the sampling parameter.
--tracing.jaeger.samplingserverurl (Default: "http://localhost:5778/sampling")
Set the sampling server url.
--tracing.jaeger.samplingtype (Default: "const")
Set the sampling type.
--tracing.jaeger.tracecontextheadername (Default: "uber-trace-id")
Set the header to use for the trace-id.
--tracing.servicename (Default: "traefik")
Set the name for this service.
--tracing.spannamelimit (Default: "0")
Set the maximum character limit for Span names (default 0 = no limit).
--tracing.zipkin (Default: "false")
Settings for zipkin.
--tracing.zipkin.debug (Default: "false")
Enable Zipkin debug.
--tracing.zipkin.httpendpoint (Default: "http://localhost:9411/api/v1/spans")
HTTP Endpoint to report traces to.
--tracing.zipkin.id128bit (Default: "true")
Use Zipkin 128 bit root span IDs.
--tracing.zipkin.samespan (Default: "false")
Use Zipkin SameSpan RPC style traces.
--tracing.zipkin.samplerate (Default: "1.000000")
The rate between 0.0 and 1.0 of requests to trace.

View file

@ -0,0 +1,616 @@
# Static Configuration: Environment variables
`TRAEFIK_ACCESSLOG`:
Access log settings. (Default: ```false```)
`TRAEFIK_ACCESSLOG_BUFFERINGSIZE`:
Number of access log lines to process in a buffered way. (Default: ```0```)
`TRAEFIK_ACCESSLOG_FIELDS_DEFAULTMODE`:
Default mode for fields: keep | drop (Default: ```keep```)
`TRAEFIK_ACCESSLOG_FIELDS_HEADERS_DEFAULTMODE`:
Default mode for fields: keep | drop | redact (Default: ```keep```)
`TRAEFIK_ACCESSLOG_FIELDS_HEADERS_NAMES_<NAME>`:
Override mode for headers
`TRAEFIK_ACCESSLOG_FIELDS_NAMES_<NAME>`:
Override mode for fields
`TRAEFIK_ACCESSLOG_FILEPATH`:
Access log file path. Stdout is used when omitted or empty.
`TRAEFIK_ACCESSLOG_FILTERS_MINDURATION`:
Keep access logs when request took longer than the specified duration. (Default: ```0```)
`TRAEFIK_ACCESSLOG_FILTERS_RETRYATTEMPTS`:
Keep access logs when at least one retry happened. (Default: ```false```)
`TRAEFIK_ACCESSLOG_FILTERS_STATUSCODES`:
Keep access logs with status codes in the specified range.
`TRAEFIK_ACCESSLOG_FORMAT`:
Access log format: json | common (Default: ```common```)
`TRAEFIK_ACME_ACMELOGGING`:
Enable debug logging of ACME actions. (Default: ```false```)
`TRAEFIK_ACME_CASERVER`:
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
`TRAEFIK_ACME_DNSCHALLENGE`:
Activate DNS-01 Challenge. (Default: ```false```)
`TRAEFIK_ACME_DNSCHALLENGE_DELAYBEFORECHECK`:
Assume DNS propagates after a delay in seconds rather than finding and querying nameservers. (Default: ```0```)
`TRAEFIK_ACME_DNSCHALLENGE_DISABLEPROPAGATIONCHECK`:
Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended] (Default: ```false```)
`TRAEFIK_ACME_DNSCHALLENGE_PROVIDER`:
Use a DNS-01 based challenge provider rather than HTTPS.
`TRAEFIK_ACME_DNSCHALLENGE_RESOLVERS`:
Use following DNS servers to resolve the FQDN authority.
`TRAEFIK_ACME_DOMAINS`:
The list of domains for which certificates are generated on startup. Wildcard domains only accepted with DNSChallenge.
`TRAEFIK_ACME_DOMAINS[n]_MAIN`:
Default subject name.
`TRAEFIK_ACME_DOMAINS[n]_SANS`:
Subject alternative names.
`TRAEFIK_ACME_EMAIL`:
Email address used for registration.
`TRAEFIK_ACME_ENTRYPOINT`:
EntryPoint to use.
`TRAEFIK_ACME_HTTPCHALLENGE`:
Activate HTTP-01 Challenge. (Default: ```false```)
`TRAEFIK_ACME_HTTPCHALLENGE_ENTRYPOINT`:
HTTP challenge EntryPoint
`TRAEFIK_ACME_KEYTYPE`:
KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. (Default: ```RSA4096```)
`TRAEFIK_ACME_ONHOSTRULE`:
Enable certificate generation on router Host rules. (Default: ```false```)
`TRAEFIK_ACME_STORAGE`:
Storage to use. (Default: ```acme.json```)
`TRAEFIK_ACME_TLSCHALLENGE`:
Activate TLS-ALPN-01 Challenge. (Default: ```true```)
`TRAEFIK_API`:
Enable api/dashboard. (Default: ```false```)
`TRAEFIK_API_DASHBOARD`:
Activate dashboard. (Default: ```true```)
`TRAEFIK_API_ENTRYPOINT`:
EntryPoint. (Default: ```traefik```)
`TRAEFIK_API_MIDDLEWARES`:
Middleware list.
`TRAEFIK_API_STATISTICS`:
Enable more detailed statistics. (Default: ```false```)
`TRAEFIK_API_STATISTICS_RECENTERRORS`:
Number of recent errors logged. (Default: ```10```)
`TRAEFIK_CONFIGFILE`:
Configuration file to use. If specified all other flags are ignored. (Default: "")
`TRAEFIK_ENTRYPOINTS_<NAME>`:
Entry points definition. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_ADDRESS`:
Entry point address.
`TRAEFIK_ENTRYPOINTS_<NAME>_FORWARDEDHEADERS_INSECURE`:
Trust all forwarded headers. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_FORWARDEDHEADERS_TRUSTEDIPS`:
Trust only forwarded headers from selected IPs.
`TRAEFIK_ENTRYPOINTS_<NAME>_PROXYPROTOCOL`:
Proxy-Protocol configuration. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_PROXYPROTOCOL_INSECURE`:
Trust all. (Default: ```false```)
`TRAEFIK_ENTRYPOINTS_<NAME>_PROXYPROTOCOL_TRUSTEDIPS`:
Trust only selected IPs.
`TRAEFIK_ENTRYPOINTS_<NAME>_TRANSPORT_LIFECYCLE_GRACETIMEOUT`:
Duration to give active requests a chance to finish before Traefik stops. (Default: ```10```)
`TRAEFIK_ENTRYPOINTS_<NAME>_TRANSPORT_LIFECYCLE_REQUESTACCEPTGRACETIMEOUT`:
Duration to keep accepting requests before Traefik initiates the graceful shutdown procedure. (Default: ```0```)
`TRAEFIK_ENTRYPOINTS_<NAME>_TRANSPORT_RESPONDINGTIMEOUTS_IDLETIMEOUT`:
IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set. (Default: ```180```)
`TRAEFIK_ENTRYPOINTS_<NAME>_TRANSPORT_RESPONDINGTIMEOUTS_READTIMEOUT`:
ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set. (Default: ```0```)
`TRAEFIK_ENTRYPOINTS_<NAME>_TRANSPORT_RESPONDINGTIMEOUTS_WRITETIMEOUT`:
WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```)
`TRAEFIK_GLOBAL_CHECKNEWVERSION`:
Periodically check if a new version has been released. (Default: ```false```)
`TRAEFIK_GLOBAL_DEBUG`:
Enable debug mode. (Default: ```false```)
`TRAEFIK_GLOBAL_SENDANONYMOUSUSAGE`:
Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default.
`TRAEFIK_HOSTRESOLVER`:
Enable CNAME Flattening. (Default: ```false```)
`TRAEFIK_HOSTRESOLVER_CNAMEFLATTENING`:
A flag to enable/disable CNAME flattening (Default: ```false```)
`TRAEFIK_HOSTRESOLVER_RESOLVCONFIG`:
resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
`TRAEFIK_HOSTRESOLVER_RESOLVDEPTH`:
The maximal depth of DNS recursive resolving (Default: ```5```)
`TRAEFIK_LOG_FILEPATH`:
Traefik log file path. Stdout is used when omitted or empty.
`TRAEFIK_LOG_FORMAT`:
Traefik log format: json | common (Default: ```common```)
`TRAEFIK_LOG_LEVEL`:
Log level set to traefik logs. (Default: ```ERROR```)
`TRAEFIK_METRICS_DATADOG`:
DataDog metrics exporter type. (Default: ```false```)
`TRAEFIK_METRICS_DATADOG_ADDRESS`:
DataDog's address. (Default: ```localhost:8125```)
`TRAEFIK_METRICS_DATADOG_PUSHINTERVAL`:
DataDog push interval. (Default: ```10```)
`TRAEFIK_METRICS_INFLUXDB`:
InfluxDB metrics exporter type. (Default: ```false```)
`TRAEFIK_METRICS_INFLUXDB_ADDRESS`:
InfluxDB address. (Default: ```localhost:8089```)
`TRAEFIK_METRICS_INFLUXDB_DATABASE`:
InfluxDB database used when protocol is http.
`TRAEFIK_METRICS_INFLUXDB_PASSWORD`:
InfluxDB password (only with http).
`TRAEFIK_METRICS_INFLUXDB_PROTOCOL`:
InfluxDB address protocol (udp or http). (Default: ```udp```)
`TRAEFIK_METRICS_INFLUXDB_PUSHINTERVAL`:
InfluxDB push interval. (Default: ```10```)
`TRAEFIK_METRICS_INFLUXDB_RETENTIONPOLICY`:
InfluxDB retention policy used when protocol is http.
`TRAEFIK_METRICS_INFLUXDB_USERNAME`:
InfluxDB username (only with http).
`TRAEFIK_METRICS_PROMETHEUS`:
Prometheus metrics exporter type. (Default: ```false```)
`TRAEFIK_METRICS_PROMETHEUS_BUCKETS`:
Buckets for latency metrics. (Default: ```0.100000, 0.300000, 1.200000, 5.000000```)
`TRAEFIK_METRICS_PROMETHEUS_ENTRYPOINT`:
EntryPoint. (Default: ```traefik```)
`TRAEFIK_METRICS_PROMETHEUS_MIDDLEWARES`:
Middlewares.
`TRAEFIK_METRICS_STATSD`:
StatsD metrics exporter type. (Default: ```false```)
`TRAEFIK_METRICS_STATSD_ADDRESS`:
StatsD address. (Default: ```localhost:8125```)
`TRAEFIK_METRICS_STATSD_PUSHINTERVAL`:
StatsD push interval. (Default: ```10```)
`TRAEFIK_PING`:
Enable ping. (Default: ```false```)
`TRAEFIK_PING_ENTRYPOINT`:
Ping entryPoint. (Default: ```traefik```)
`TRAEFIK_PING_MIDDLEWARES`:
Middleware list.
`TRAEFIK_PROVIDERS_DOCKER`:
Enable Docker backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS`:
Filter services by constraint, matching with Traefik tags.
`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS[n]_KEY`:
The provider label that will be matched against. In practice, it is always 'tag'.
`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS[n]_MUSTMATCH`:
Whether the matching operator is equals or not equals. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS[n]_VALUE`:
The value that will be matched against.
`TRAEFIK_PROVIDERS_DOCKER_DEFAULTRULE`:
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
`TRAEFIK_PROVIDERS_DOCKER_ENDPOINT`:
Docker server endpoint. Can be a tcp or a unix socket endpoint. (Default: ```unix:///var/run/docker.sock```)
`TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT`:
Expose containers by default. (Default: ```true```)
`TRAEFIK_PROVIDERS_DOCKER_NETWORK`:
Default Docker network used.
`TRAEFIK_PROVIDERS_DOCKER_SWARMMODE`:
Use Docker on Swarm Mode. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_SWARMMODEREFRESHSECONDS`:
Polling interval for swarm mode. (Default: ```15```)
`TRAEFIK_PROVIDERS_DOCKER_TLS_CA`:
TLS CA
`TRAEFIK_PROVIDERS_DOCKER_TLS_CAOPTIONAL`:
TLS CA.Optional (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_TLS_CERT`:
TLS cert
`TRAEFIK_PROVIDERS_DOCKER_TLS_INSECURESKIPVERIFY`:
TLS insecure skip verify (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_TLS_KEY`:
TLS key
`TRAEFIK_PROVIDERS_DOCKER_USEBINDPORTIP`:
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_WATCH`:
Watch provider. (Default: ```true```)
`TRAEFIK_PROVIDERS_FILE`:
Enable File backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_FILE_DEBUGLOGGENERATEDTEMPLATE`:
Enable debug logging of generated configuration template. (Default: ```false```)
`TRAEFIK_PROVIDERS_FILE_DIRECTORY`:
Load configuration from one or more .toml files in a directory.
`TRAEFIK_PROVIDERS_FILE_FILENAME`:
Override default configuration template. For advanced users :)
`TRAEFIK_PROVIDERS_FILE_WATCH`:
Watch provider. (Default: ```true```)
`TRAEFIK_PROVIDERS_KUBERNETES`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETESCRD_DISABLEPASSHOSTHEADERS`:
Kubernetes disable PassHost Headers. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ENDPOINT`:
Kubernetes server endpoint (required for external cluster client).
`TRAEFIK_PROVIDERS_KUBERNETESCRD_INGRESSCLASS`:
Value of kubernetes.io/ingress.class annotation to watch for.
`TRAEFIK_PROVIDERS_KUBERNETESCRD_LABELSELECTOR`:
Kubernetes label selector to use.
`TRAEFIK_PROVIDERS_KUBERNETESCRD_NAMESPACES`:
Kubernetes namespaces.
`TRAEFIK_PROVIDERS_KUBERNETESCRD_TOKEN`:
Kubernetes bearer token (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETES_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETES_DISABLEPASSHOSTHEADERS`:
Kubernetes disable PassHost Headers. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETES_ENDPOINT`:
Kubernetes server endpoint (required for external cluster client).
`TRAEFIK_PROVIDERS_KUBERNETES_INGRESSCLASS`:
Value of kubernetes.io/ingress.class annotation to watch for.
`TRAEFIK_PROVIDERS_KUBERNETES_INGRESSENDPOINT_HOSTNAME`:
Hostname used for Kubernetes Ingress endpoints.
`TRAEFIK_PROVIDERS_KUBERNETES_INGRESSENDPOINT_IP`:
IP used for Kubernetes Ingress endpoints.
`TRAEFIK_PROVIDERS_KUBERNETES_INGRESSENDPOINT_PUBLISHEDSERVICE`:
Published Kubernetes Service to copy status from.
`TRAEFIK_PROVIDERS_KUBERNETES_LABELSELECTOR`:
Kubernetes Ingress label selector to use.
`TRAEFIK_PROVIDERS_KUBERNETES_NAMESPACES`:
Kubernetes namespaces.
`TRAEFIK_PROVIDERS_KUBERNETES_TOKEN`:
Kubernetes bearer token (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_MARATHON`:
Enable Marathon backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_BASIC_HTTPBASICAUTHUSER`:
Basic authentication User.
`TRAEFIK_PROVIDERS_MARATHON_BASIC_HTTPBASICPASSWORD`:
Basic authentication Password.
`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS`:
Filter services by constraint, matching with Traefik tags.
`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS[n]_KEY`:
The provider label that will be matched against. In practice, it is always 'tag'.
`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS[n]_MUSTMATCH`:
Whether the matching operator is equals or not equals. (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS[n]_VALUE`:
The value that will be matched against.
`TRAEFIK_PROVIDERS_MARATHON_DCOSTOKEN`:
DCOSToken for DCOS environment, This will override the Authorization header.
`TRAEFIK_PROVIDERS_MARATHON_DEFAULTRULE`:
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
`TRAEFIK_PROVIDERS_MARATHON_DIALERTIMEOUT`:
Set a dialer timeout for Marathon. (Default: ```5```)
`TRAEFIK_PROVIDERS_MARATHON_ENDPOINT`:
Marathon server endpoint. You can also specify multiple endpoint for Marathon. (Default: ```http://127.0.0.1:8080```)
`TRAEFIK_PROVIDERS_MARATHON_EXPOSEDBYDEFAULT`:
Expose Marathon apps by default. (Default: ```true```)
`TRAEFIK_PROVIDERS_MARATHON_FILTERMARATHONCONSTRAINTS`:
Enable use of Marathon constraints in constraint filtering. (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_FORCETASKHOSTNAME`:
Force to use the task's hostname. (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_KEEPALIVE`:
Set a TCP Keep Alive time. (Default: ```10```)
`TRAEFIK_PROVIDERS_MARATHON_RESPECTREADINESSCHECKS`:
Filter out tasks with non-successful readiness checks during deployments. (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_RESPONSEHEADERTIMEOUT`:
Set a response header timeout for Marathon. (Default: ```60```)
`TRAEFIK_PROVIDERS_MARATHON_TLSHANDSHAKETIMEOUT`:
Set a TLS handshake timeout for Marathon. (Default: ```5```)
`TRAEFIK_PROVIDERS_MARATHON_TLS_CA`:
TLS CA
`TRAEFIK_PROVIDERS_MARATHON_TLS_CAOPTIONAL`:
TLS CA.Optional (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_TLS_CERT`:
TLS cert
`TRAEFIK_PROVIDERS_MARATHON_TLS_INSECURESKIPVERIFY`:
TLS insecure skip verify (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_TLS_KEY`:
TLS key
`TRAEFIK_PROVIDERS_MARATHON_TRACE`:
Display additional provider logs. (Default: ```false```)
`TRAEFIK_PROVIDERS_MARATHON_WATCH`:
Watch provider. (Default: ```true```)
`TRAEFIK_PROVIDERS_PROVIDERSTHROTTLEDURATION`:
Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time. (Default: ```0```)
`TRAEFIK_PROVIDERS_RANCHER`:
Enable Rancher backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS`:
Filter services by constraint, matching with Traefik tags.
`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS[n]_KEY`:
The provider label that will be matched against. In practice, it is always 'tag'.
`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS[n]_MUSTMATCH`:
Whether the matching operator is equals or not equals. (Default: ```false```)
`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS[n]_VALUE`:
The value that will be matched against.
`TRAEFIK_PROVIDERS_RANCHER_DEFAULTRULE`:
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
`TRAEFIK_PROVIDERS_RANCHER_ENABLESERVICEHEALTHFILTER`:
Filter services with unhealthy states and inactive states. (Default: ```true```)
`TRAEFIK_PROVIDERS_RANCHER_EXPOSEDBYDEFAULT`:
Expose containers by default. (Default: ```true```)
`TRAEFIK_PROVIDERS_RANCHER_INTERVALPOLL`:
Poll the Rancher metadata service every 'rancher.refreshseconds' (less accurate). (Default: ```false```)
`TRAEFIK_PROVIDERS_RANCHER_PREFIX`:
Prefix used for accessing the Rancher metadata service. (Default: ```latest```)
`TRAEFIK_PROVIDERS_RANCHER_REFRESHSECONDS`:
Defines the polling interval in seconds. (Default: ```15```)
`TRAEFIK_PROVIDERS_RANCHER_WATCH`:
Watch provider. (Default: ```true```)
`TRAEFIK_PROVIDERS_REST`:
Enable Rest backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_REST_ENTRYPOINT`:
EntryPoint. (Default: ```traefik```)
`TRAEFIK_SERVERSTRANSPORT_FORWARDINGTIMEOUTS_DIALTIMEOUT`:
The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists. (Default: ```30```)
`TRAEFIK_SERVERSTRANSPORT_FORWARDINGTIMEOUTS_RESPONSEHEADERTIMEOUT`:
The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists. (Default: ```0```)
`TRAEFIK_SERVERSTRANSPORT_INSECURESKIPVERIFY`:
Disable SSL certificate verification. (Default: ```false```)
`TRAEFIK_SERVERSTRANSPORT_MAXIDLECONNSPERHOST`:
If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used (Default: ```0```)
`TRAEFIK_SERVERSTRANSPORT_ROOTCAS`:
Add cert file for self-signed certificate.
`TRAEFIK_TRACING`:
OpenTracing configuration. (Default: ```false```)
`TRAEFIK_TRACING_BACKEND`:
Selects the tracking backend ('jaeger','zipkin','datadog','instana'). (Default: ```jaeger```)
`TRAEFIK_TRACING_DATADOG`:
Settings for DataDog. (Default: ```false```)
`TRAEFIK_TRACING_DATADOG_BAGAGEPREFIXHEADERNAME`:
Specifies the header name prefix that will be used to store baggage items in a map.
`TRAEFIK_TRACING_DATADOG_DEBUG`:
Enable DataDog debug. (Default: ```false```)
`TRAEFIK_TRACING_DATADOG_GLOBALTAG`:
Key:Value tag to be set on all the spans.
`TRAEFIK_TRACING_DATADOG_LOCALAGENTHOSTPORT`:
Set datadog-agent's host:port that the reporter will used. (Default: ```localhost:8126```)
`TRAEFIK_TRACING_DATADOG_PARENTIDHEADERNAME`:
Specifies the header name that will be used to store the parent ID.
`TRAEFIK_TRACING_DATADOG_PRIORITYSAMPLING`:
Enable priority sampling. When using distributed tracing, this option must be enabled in order to get all the parts of a distributed trace sampled. (Default: ```false```)
`TRAEFIK_TRACING_DATADOG_SAMPLINGPRIORITYHEADERNAME`:
Specifies the header name that will be used to store the sampling priority.
`TRAEFIK_TRACING_DATADOG_TRACEIDHEADERNAME`:
Specifies the header name that will be used to store the trace ID.
`TRAEFIK_TRACING_HAYSTACK`:
Settings for Haystack. (Default: ```false```)
`TRAEFIK_TRACING_HAYSTACK_BAGGAGEPREFIXHEADERNAME`:
specifies the header name prefix that will be used to store baggage items in a map.
`TRAEFIK_TRACING_HAYSTACK_GLOBALTAG`:
Key:Value tag to be set on all the spans.
`TRAEFIK_TRACING_HAYSTACK_LOCALAGENTHOST`:
Set haystack-agent's host that the reporter will used. (Default: ```LocalAgentHost```)
`TRAEFIK_TRACING_HAYSTACK_LOCALAGENTPORT`:
Set haystack-agent's port that the reporter will used. (Default: ```35000```)
`TRAEFIK_TRACING_HAYSTACK_PARENTIDHEADERNAME`:
Specifies the header name that will be used to store the parent ID.
`TRAEFIK_TRACING_HAYSTACK_SPANIDHEADERNAME`:
Specifies the header name that will be used to store the span ID.
`TRAEFIK_TRACING_HAYSTACK_TRACEIDHEADERNAME`:
Specifies the header name that will be used to store the trace ID.
`TRAEFIK_TRACING_INSTANA`:
Settings for Instana. (Default: ```false```)
`TRAEFIK_TRACING_INSTANA_LOCALAGENTHOST`:
Set instana-agent's host that the reporter will used. (Default: ```localhost```)
`TRAEFIK_TRACING_INSTANA_LOCALAGENTPORT`:
Set instana-agent's port that the reporter will used. (Default: ```42699```)
`TRAEFIK_TRACING_INSTANA_LOGLEVEL`:
Set instana-agent's log level. ('error','warn','info','debug') (Default: ```info```)
`TRAEFIK_TRACING_JAEGER`:
Settings for jaeger. (Default: ```false```)
`TRAEFIK_TRACING_JAEGER_GEN128BIT`:
Generate 128 bit span IDs. (Default: ```false```)
`TRAEFIK_TRACING_JAEGER_LOCALAGENTHOSTPORT`:
Set jaeger-agent's host:port that the reporter will used. (Default: ```127.0.0.1:6831```)
`TRAEFIK_TRACING_JAEGER_PROPAGATION`:
Which propgation format to use (jaeger/b3). (Default: ```jaeger```)
`TRAEFIK_TRACING_JAEGER_SAMPLINGPARAM`:
Set the sampling parameter. (Default: ```1.000000```)
`TRAEFIK_TRACING_JAEGER_SAMPLINGSERVERURL`:
Set the sampling server url. (Default: ```http://localhost:5778/sampling```)
`TRAEFIK_TRACING_JAEGER_SAMPLINGTYPE`:
Set the sampling type. (Default: ```const```)
`TRAEFIK_TRACING_JAEGER_TRACECONTEXTHEADERNAME`:
Set the header to use for the trace-id. (Default: ```uber-trace-id```)
`TRAEFIK_TRACING_SERVICENAME`:
Set the name for this service. (Default: ```traefik```)
`TRAEFIK_TRACING_SPANNAMELIMIT`:
Set the maximum character limit for Span names (default 0 = no limit). (Default: ```0```)
`TRAEFIK_TRACING_ZIPKIN`:
Settings for zipkin. (Default: ```false```)
`TRAEFIK_TRACING_ZIPKIN_DEBUG`:
Enable Zipkin debug. (Default: ```false```)
`TRAEFIK_TRACING_ZIPKIN_HTTPENDPOINT`:
HTTP Endpoint to report traces to. (Default: ```http://localhost:9411/api/v1/spans```)
`TRAEFIK_TRACING_ZIPKIN_ID128BIT`:
Use Zipkin 128 bit root span IDs. (Default: ```true```)
`TRAEFIK_TRACING_ZIPKIN_SAMESPAN`:
Use Zipkin SameSpan RPC style traces. (Default: ```false```)
`TRAEFIK_TRACING_ZIPKIN_SAMPLERATE`:
The rate between 0.0 and 1.0 of requests to trace. (Default: ```1.000000```)

View file

@ -0,0 +1,7 @@
# Static Configuration: File
## TOML
```toml
--8<-- "content/reference/static-configuration/file.toml"
```

View file

@ -0,0 +1,5 @@
# Static Configuration
- [File](./file.md)
- [CLI](./cli.md)
- [Environment Variables](./env.md)

View file

@ -47,7 +47,7 @@ See the complete reference for the list of available options:
[EntryPoints] [EntryPoints]
[EntryPoints.EntryPoint0] [EntryPoints.EntryPoint0]
Address = "foobar" Address = ":8888"
[EntryPoints.EntryPoint0.Transport] [EntryPoints.EntryPoint0.Transport]
[EntryPoints.EntryPoint0.Transport.LifeCycle] [EntryPoints.EntryPoint0.Transport.LifeCycle]
RequestAcceptGraceTimeout = 42 RequestAcceptGraceTimeout = 42
@ -65,52 +65,18 @@ See the complete reference for the list of available options:
``` ```
```ini tab="CLI" ```ini tab="CLI"
Name:EntryPoint0 --entryPoints.EntryPoint0.Address=:8888
Address:foobar --entryPoints.EntryPoint0.Transport.LifeCycle.RequestAcceptGraceTimeout=42
Transport.LifeCycle.RequestAcceptGraceTimeout:42 --entryPoints.EntryPoint0.Transport.LifeCycle.GraceTimeOut=42
Transport.LifeCycle.GraceTimeOut:42 --entryPoints.EntryPoint0.Transport.RespondingTimeouts.ReadTimeout=42
Transport.RespondingTimeouts.ReadTimeout:42 --entryPoints.EntryPoint0.Transport.RespondingTimeouts.WriteTimeout=42
Transport.RespondingTimeouts.WriteTimeout:42 --entryPoints.EntryPoint0.Transport.RespondingTimeouts.IdleTimeout=42
Transport.RespondingTimeouts.IdleTimeout:42 --entryPoints.EntryPoint0.ProxyProtocol.Insecure=true
ProxyProtocol.Insecure:true --entryPoints.EntryPoint0.ProxyProtocol.TrustedIPs=foobar,foobar
ProxyProtocol.TrustedIPs:foobar,foobar --entryPoints.EntryPoint0.ForwardedHeaders.Insecure=true
ForwardedHeaders.Insecure:true --entryPoints.EntryPoint0.ForwardedHeaders.TrustedIPs=foobar,foobar
ForwardedHeaders.TrustedIPs:foobar,foobar
``` ```
??? example "Using the CLI"
Here is an example of using the CLI to define `entrypoints`:
```shell
--entryPoints='Name:http Address::80'
--entryPoints='Name:https Address::443'
```
!!! note
The whitespace character (` `) is the option separator, and the comma (`,`) is the value separator for lists inside an option.
The option names are case-insensitive.
!!! warning "Using Docker Compose Files"
The syntax for passing arguments inside a docker compose file is a little different. Here are two examples.
```yaml
traefik:
image: traefik:v2.0 # The official v2.0 Traefik docker image
command:
- --defaultentrypoints=powpow
- "--entryPoints=Name:powpow Address::42 Compress:true"
```
or
```yaml
traefik:
image: traefik:v2.0 # The official v2.0 Traefik docker image
command: --defaultentrypoints=powpow --entryPoints='Name:powpow Address::42 Compress:true'
```
## ProxyProtocol ## ProxyProtocol
Traefik supports [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). Traefik supports [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt).
@ -128,9 +94,10 @@ Traefik supports [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-
IPs in `trustedIPs` only will lead to remote client address replacement: Declare load-balancer IPs or CIDR range here. IPs in `trustedIPs` only will lead to remote client address replacement: Declare load-balancer IPs or CIDR range here.
??? example "Insecure Mode -- Testing Environnement Only" ??? example "Insecure Mode -- Testing Environment Only"
In a test environments, you can configure Traefik to trust every incoming connection. Doing so, every remote client address will be replaced (`trustedIPs` won't have any effect) In a test environments, you can configure Traefik to trust every incoming connection.
Doing so, every remote client address will be replaced (`trustedIPs` won't have any effect)
```toml ```toml
[entryPoints] [entryPoints]

View file

@ -30,8 +30,8 @@ spec:
args: args:
- --api - --api
- --accesslog - --accesslog
- --entrypoints=Name:web Address::8000 - --entrypoints.web.Address=:8000
- --entrypoints=Name:websecure Address::4443 - --entrypoints.websecure.Address=:4443
- --providers.kubernetescrd - --providers.kubernetescrd
- --providers.kubernetescrd.trace - --providers.kubernetescrd.trace
- --acme - --acme

View file

@ -133,7 +133,11 @@ nav:
- 'Maintainers': 'contributing/maintainers.md' - 'Maintainers': 'contributing/maintainers.md'
- 'Glossary': 'glossary.md' - 'Glossary': 'glossary.md'
- 'References': - 'References':
- 'Static Configuration': 'reference/static-configuration.md' - 'Static Configuration':
- 'Overview': 'reference/static-configuration/overview.md'
- 'File': 'reference/static-configuration/file.md'
- 'CLI': 'reference/static-configuration/cli.md'
- 'Environment variables': 'reference/static-configuration/env.md'
- 'Dynamic Configuration': - 'Dynamic Configuration':
- 'Docker': 'reference/dynamic-configuration/docker.md' - 'Docker': 'reference/dynamic-configuration/docker.md'
- 'Kubernetes CRD': 'reference/dynamic-configuration/kubernetes-crd.md' - 'Kubernetes CRD': 'reference/dynamic-configuration/kubernetes-crd.md'

View file

@ -10,8 +10,6 @@ level = "DEBUG"
address = ":8000" address = ":8000"
[entryPoints.web.ForwardedHeaders] [entryPoints.web.ForwardedHeaders]
insecure=true insecure=true
[entryPoints.web.ClientIPStrategy]
depth=2
[api] [api]

View file

@ -10,6 +10,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"text/template" "text/template"
@ -144,7 +145,7 @@ func (s *BaseSuite) adaptFile(c *check.C, path string, tempObjects interface{})
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
folder, prefix := filepath.Split(path) folder, prefix := filepath.Split(path)
tmpFile, err := ioutil.TempFile(folder, prefix) tmpFile, err := ioutil.TempFile(folder, strings.TrimSuffix(prefix, filepath.Ext(prefix))+"_*"+filepath.Ext(prefix))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer tmpFile.Close() defer tmpFile.Close()

View file

@ -161,7 +161,7 @@ func (s *SimpleSuite) TestApiOnSameEntryPoint(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints=Name:http Address::8000", "--api.entryPoint=http", "--global.debug", "--providers.docker") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--api.entryPoint=http", "--global.debug", "--providers.docker")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -241,7 +241,7 @@ func (s *SimpleSuite) TestDefaultEntrypointHTTP(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints=Name:http Address::8000", "--global.debug", "--providers.docker", "--api") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--global.debug", "--providers.docker", "--api")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -259,7 +259,7 @@ func (s *SimpleSuite) TestWithUnexistingEntrypoint(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints=Name:http Address::8000", "--global.debug", "--providers.docker", "--api") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--global.debug", "--providers.docker", "--api")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -277,7 +277,7 @@ func (s *SimpleSuite) TestMetricsPrometheusDefaultEntrypoint(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints=Name:http Address::8000", "--api", "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0", "--providers.docker", "--global.debug") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--api", "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0", "--providers.docker", "--global.debug")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -346,19 +346,6 @@ func (s *SimpleSuite) TestIPStrategyWhitelist(c *check.C) {
host string host string
expectedStatusCode int expectedStatusCode int
}{ }{
// {
// desc: "default client ip strategy accept",
// xForwardedFor: "8.8.8.8,127.0.0.1",
// host: "no.override.whitelist.docker.local",
// expectedStatusCode: 200,
// },
// FIXME add clientipstrategy and forwarded headers on entrypoint
// {
// desc: "default client ip strategy reject",
// xForwardedFor: "8.8.8.10,127.0.0.1",
// host: "no.override.whitelist.docker.local",
// expectedStatusCode: 403,
// },
{ {
desc: "override remote addr reject", desc: "override remote addr reject",
xForwardedFor: "8.8.8.8,8.8.8.8", xForwardedFor: "8.8.8.8,8.8.8.8",

View file

@ -5,7 +5,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/ping" "github.com/containous/traefik/pkg/ping"
"github.com/containous/traefik/pkg/provider" "github.com/containous/traefik/pkg/provider"
@ -38,18 +37,18 @@ func TestDo_globalConfiguration(t *testing.T) {
FilePath: "AccessLog FilePath", FilePath: "AccessLog FilePath",
Format: "AccessLog Format", Format: "AccessLog Format",
Filters: &types.AccessLogFilters{ Filters: &types.AccessLogFilters{
StatusCodes: types.StatusCodes{"200", "500"}, StatusCodes: []string{"200", "500"},
RetryAttempts: true, RetryAttempts: true,
MinDuration: 10, MinDuration: 10,
}, },
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldNames{ Names: map[string]string{
"RequestHost": "keep", "RequestHost": "keep",
}, },
Headers: &types.FieldHeaders{ Headers: &types.FieldHeaders{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldHeaderNames{ Names: map[string]string{
"Referer": "keep", "Referer": "keep",
}, },
}, },
@ -68,9 +67,9 @@ func TestDo_globalConfiguration(t *testing.T) {
Address: "foo Address", Address: "foo Address",
Transport: &static.EntryPointsTransport{ Transport: &static.EntryPointsTransport{
RespondingTimeouts: &static.RespondingTimeouts{ RespondingTimeouts: &static.RespondingTimeouts{
ReadTimeout: parse.Duration(111 * time.Second), ReadTimeout: types.Duration(111 * time.Second),
WriteTimeout: parse.Duration(111 * time.Second), WriteTimeout: types.Duration(111 * time.Second),
IdleTimeout: parse.Duration(111 * time.Second), IdleTimeout: types.Duration(111 * time.Second),
}, },
}, },
ProxyProtocol: &static.ProxyProtocol{ ProxyProtocol: &static.ProxyProtocol{
@ -81,9 +80,9 @@ func TestDo_globalConfiguration(t *testing.T) {
Address: "fii Address", Address: "fii Address",
Transport: &static.EntryPointsTransport{ Transport: &static.EntryPointsTransport{
RespondingTimeouts: &static.RespondingTimeouts{ RespondingTimeouts: &static.RespondingTimeouts{
ReadTimeout: parse.Duration(111 * time.Second), ReadTimeout: types.Duration(111 * time.Second),
WriteTimeout: parse.Duration(111 * time.Second), WriteTimeout: types.Duration(111 * time.Second),
IdleTimeout: parse.Duration(111 * time.Second), IdleTimeout: types.Duration(111 * time.Second),
}, },
}, },
ProxyProtocol: &static.ProxyProtocol{ ProxyProtocol: &static.ProxyProtocol{
@ -112,16 +111,16 @@ func TestDo_globalConfiguration(t *testing.T) {
}, },
} }
config.Providers = &static.Providers{ config.Providers = &static.Providers{
ProvidersThrottleDuration: parse.Duration(111 * time.Second), ProvidersThrottleDuration: types.Duration(111 * time.Second),
} }
config.ServersTransport = &static.ServersTransport{ config.ServersTransport = &static.ServersTransport{
InsecureSkipVerify: true, InsecureSkipVerify: true,
RootCAs: traefiktls.FilesOrContents{"RootCAs 1", "RootCAs 2", "RootCAs 3"}, RootCAs: []traefiktls.FileOrContent{"RootCAs 1", "RootCAs 2", "RootCAs 3"},
MaxIdleConnsPerHost: 111, MaxIdleConnsPerHost: 111,
ForwardingTimeouts: &static.ForwardingTimeouts{ ForwardingTimeouts: &static.ForwardingTimeouts{
DialTimeout: parse.Duration(111 * time.Second), DialTimeout: types.Duration(111 * time.Second),
ResponseHeaderTimeout: parse.Duration(111 * time.Second), ResponseHeaderTimeout: types.Duration(111 * time.Second),
}, },
} }
@ -156,15 +155,15 @@ func TestDo_globalConfiguration(t *testing.T) {
config.Providers.Docker = &docker.Provider{ config.Providers.Docker = &docker.Provider{
Constrainer: provider.Constrainer{ Constrainer: provider.Constrainer{
Constraints: types.Constraints{ Constraints: []*types.Constraint{
{ {
Key: "file Constraints Key 1", Key: "file Constraints Key 1",
Regex: "file Constraints Regex 2", Value: "file Constraints Regex 2",
MustMatch: true, MustMatch: true,
}, },
{ {
Key: "file Constraints Key 1", Key: "file Constraints Key 1",
Regex: "file Constraints Regex 2", Value: "file Constraints Regex 2",
MustMatch: true, MustMatch: true,
}, },
}, },
@ -210,22 +209,22 @@ func TestDo_globalConfiguration(t *testing.T) {
config.Metrics = &types.Metrics{ config.Metrics = &types.Metrics{
Prometheus: &types.Prometheus{ Prometheus: &types.Prometheus{
Buckets: types.Buckets{0.1, 0.3, 1.2, 5}, Buckets: []float64{0.1, 0.3, 1.2, 5},
EntryPoint: "MyEntryPoint", EntryPoint: "MyEntryPoint",
Middlewares: []string{"m1", "m2"}, Middlewares: []string{"m1", "m2"},
}, },
Datadog: &types.Datadog{ Datadog: &types.Datadog{
Address: "localhost:8181", Address: "localhost:8181",
PushInterval: "12", PushInterval: 12,
}, },
StatsD: &types.Statsd{ StatsD: &types.Statsd{
Address: "localhost:8182", Address: "localhost:8182",
PushInterval: "42", PushInterval: 42,
}, },
InfluxDB: &types.InfluxDB{ InfluxDB: &types.InfluxDB{
Address: "localhost:8183", Address: "localhost:8183",
Protocol: "http", Protocol: "http",
PushInterval: "22", PushInterval: 22,
Database: "myDB", Database: "myDB",
RetentionPolicy: "12", RetentionPolicy: "12",
Username: "a", Username: "a",

115
pkg/cli/commands.go Normal file
View file

@ -0,0 +1,115 @@
// Package cli provides tools to create commands that support advanced configuration features,
// sub-commands, and allowing configuration from command-line flags, configuration files, and environment variables.
package cli
import (
"errors"
"fmt"
"os"
"path/filepath"
)
// Command structure contains program/command information (command name and description).
type Command struct {
Name string
Description string
Configuration interface{}
Resources []ResourceLoader
Run func([]string) error
Hidden bool
subCommands []*Command
}
// AddCommand Adds a sub command.
func (c *Command) AddCommand(cmd *Command) error {
if c == nil || cmd == nil {
return nil
}
if c.Name == cmd.Name {
return fmt.Errorf("child command cannot have the same name as their parent: %s", cmd.Name)
}
c.subCommands = append(c.subCommands, cmd)
return nil
}
// Execute Executes a command.
func Execute(cmd *Command) error {
return execute(cmd, os.Args, true)
}
func execute(cmd *Command, args []string, root bool) error {
if len(args) == 1 {
if err := run(cmd, args); err != nil {
return fmt.Errorf("command %s error: %v", args[0], err)
}
return nil
}
if root && cmd.Name != args[1] && !contains(cmd.subCommands, args[1]) {
if err := run(cmd, args[1:]); err != nil {
return fmt.Errorf("command %s error: %v", filepath.Base(args[0]), err)
}
return nil
}
if len(args) >= 2 && cmd.Name == args[1] {
if err := run(cmd, args[2:]); err != nil {
return fmt.Errorf("command %s error: %v", cmd.Name, err)
}
return nil
}
if len(cmd.subCommands) == 0 {
if err := run(cmd, args[1:]); err != nil {
return fmt.Errorf("command %s error: %v", cmd.Name, err)
}
return nil
}
for _, subCmd := range cmd.subCommands {
if len(args) >= 2 && subCmd.Name == args[1] {
return execute(subCmd, args[1:], false)
}
}
return fmt.Errorf("command not found: %v", args)
}
func run(cmd *Command, args []string) error {
if isHelp(args) {
return PrintHelp(os.Stdout, cmd)
}
if cmd.Run == nil {
_ = PrintHelp(os.Stdout, cmd)
return errors.New("command not found")
}
if cmd.Configuration == nil {
return cmd.Run(args)
}
for _, resource := range cmd.Resources {
done, err := resource.Load(args, cmd)
if err != nil {
return err
}
if done {
break
}
}
return cmd.Run(args)
}
func contains(cmds []*Command, name string) bool {
for _, cmd := range cmds {
if cmd.Name == name {
return true
}
}
return false
}

681
pkg/cli/commands_test.go Normal file
View file

@ -0,0 +1,681 @@
package cli
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommand_AddCommand(t *testing.T) {
testCases := []struct {
desc string
subCommand *Command
expectedError bool
}{
{
desc: "sub command nil",
subCommand: nil,
},
{
desc: "add a simple command",
subCommand: &Command{
Name: "sub",
},
},
{
desc: "add a sub command with the same name as their parent",
subCommand: &Command{
Name: "root",
},
expectedError: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rootCmd := &Command{
Name: "root",
}
err := rootCmd.AddCommand(test.subCommand)
if test.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func Test_execute(t *testing.T) {
var called string
type expected struct {
result string
error bool
}
testCases := []struct {
desc string
args []string
command func() *Command
expected expected
}{
{
desc: "root command",
args: []string{""},
command: func() *Command {
return &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called = "root"
return nil
},
}
},
expected: expected{result: "root"},
},
{
desc: "one sub command",
args: []string{"", "sub1"},
command: func() *Command {
rootCmd := &Command{
Name: "test",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
})
return rootCmd
},
expected: expected{result: "sub1"},
},
{
desc: "two sub commands",
args: []string{"", "sub2"},
command: func() *Command {
rootCmd := &Command{
Name: "test",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
})
_ = rootCmd.AddCommand(&Command{
Name: "sub2",
Description: "sub2",
Configuration: nil,
Run: func(_ []string) error {
called += "sub2"
return nil
},
})
return rootCmd
},
expected: expected{result: "sub2"},
},
{
desc: "command with sub sub command, call sub command",
args: []string{"", "sub1"},
command: func() *Command {
rootCmd := &Command{
Name: "test",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
sub1 := &Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
}
_ = rootCmd.AddCommand(sub1)
_ = sub1.AddCommand(&Command{
Name: "sub2",
Description: "sub2",
Configuration: nil,
Run: func(_ []string) error {
called += "sub2"
return nil
},
})
return rootCmd
},
expected: expected{result: "sub1"},
},
{
desc: "command with sub sub command, call sub sub command",
args: []string{"", "sub1", "sub2"},
command: func() *Command {
rootCmd := &Command{
Name: "test",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
sub1 := &Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
}
_ = rootCmd.AddCommand(sub1)
_ = sub1.AddCommand(&Command{
Name: "sub2",
Description: "sub2",
Configuration: nil,
Run: func(_ []string) error {
called += "sub2"
return nil
},
})
return rootCmd
},
expected: expected{result: "sub2"},
},
{
desc: "command with sub command, call root command explicitly",
args: []string{"", "root"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
})
return rootCmd
},
expected: expected{result: "root"},
},
{
desc: "command with sub command, call root command implicitly",
args: []string{""},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
})
return rootCmd
},
expected: expected{result: "root"},
},
{
desc: "command with sub command, call sub command which has no run",
args: []string{"", "sub1"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called += "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
})
return rootCmd
},
expected: expected{error: true},
},
{
desc: "command with sub command, call root command which has no run",
args: []string{"", "root"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
})
return rootCmd
},
expected: expected{error: true},
},
{
desc: "command with sub command, call implicitly root command which has no run",
args: []string{""},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(_ []string) error {
called += "sub1"
return nil
},
})
return rootCmd
},
expected: expected{error: true},
},
{
desc: "command with sub command, call sub command with arguments",
args: []string{"", "sub1", "foobar.txt"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called = "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(args []string) error {
called += "sub1-" + strings.Join(args, "-")
return nil
},
})
return rootCmd
},
expected: expected{result: "sub1-foobar.txt"},
},
{
desc: "command with sub command, call root command with arguments",
args: []string{"", "foobar.txt"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(args []string) error {
called += "root-" + strings.Join(args, "-")
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(args []string) error {
called += "sub1-" + strings.Join(args, "-")
return nil
},
})
return rootCmd
},
expected: expected{result: "root-foobar.txt"},
},
{
desc: "command with sub command, call sub command with flags",
args: []string{"", "sub1", "--foo=bar", "--fii=bir"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
called = "root"
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(args []string) error {
called += "sub1-" + strings.Join(args, "")
return nil
},
})
return rootCmd
},
expected: expected{result: "sub1---foo=bar--fii=bir"},
},
{
desc: "command with sub command, call explicitly root command with flags",
args: []string{"", "root", "--foo=bar", "--fii=bir"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(args []string) error {
called += "root-" + strings.Join(args, "")
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(args []string) error {
called += "sub1-" + strings.Join(args, "")
return nil
},
})
return rootCmd
},
expected: expected{result: "root---foo=bar--fii=bir"},
},
{
desc: "command with sub command, call implicitly root command with flags",
args: []string{"", "--foo=bar", "--fii=bir"},
command: func() *Command {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(args []string) error {
called += "root-" + strings.Join(args, "")
return nil
},
}
_ = rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "sub1",
Configuration: nil,
Run: func(args []string) error {
called += "sub1-" + strings.Join(args, "")
return nil
},
})
return rootCmd
},
expected: expected{result: "root---foo=bar--fii=bir"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer func() {
called = ""
}()
err := execute(test.command(), test.args, true)
if test.expected.error {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected.result, called)
}
})
}
}
func Test_execute_configuration(t *testing.T) {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
return nil
},
}
element := &Yo{
Fuu: "test",
}
sub1 := &Command{
Name: "sub1",
Description: "sub1",
Configuration: element,
Resources: []ResourceLoader{&FlagLoader{}},
Run: func(args []string) error {
return nil
},
}
err := rootCmd.AddCommand(sub1)
require.NoError(t, err)
args := []string{"", "sub1", "--foo=bar", "--fii=bir", "--yi"}
err = execute(rootCmd, args, true)
require.NoError(t, err)
expected := &Yo{
Foo: "bar",
Fii: "bir",
Fuu: "test",
Yi: &Yi{
Foo: "foo",
Fii: "fii",
},
}
assert.Equal(t, expected, element)
}
func Test_execute_configuration_file(t *testing.T) {
rootCmd := &Command{
Name: "root",
Description: "This is a test",
Configuration: nil,
Run: func(_ []string) error {
return nil
},
}
element := &Yo{
Fuu: "test",
}
sub1 := &Command{
Name: "sub1",
Description: "sub1",
Configuration: element,
Resources: []ResourceLoader{&FileLoader{}, &FlagLoader{}},
Run: func(args []string) error {
return nil
},
}
err := rootCmd.AddCommand(sub1)
require.NoError(t, err)
args := []string{"", "sub1", "--configFile=./fixtures/config.toml"}
err = execute(rootCmd, args, true)
require.NoError(t, err)
expected := &Yo{
Foo: "bar",
Fii: "bir",
Fuu: "test",
Yi: &Yi{
Foo: "foo",
Fii: "fii",
},
}
assert.Equal(t, expected, element)
}
func Test_execute_help(t *testing.T) {
element := &Yo{
Fuu: "test",
}
rooCmd := &Command{
Name: "root",
Description: "Description for root",
Configuration: element,
Run: func(args []string) error {
return nil
},
}
args := []string{"", "--help", "--foo"}
backupStdout := os.Stdout
defer func() {
os.Stdout = backupStdout
}()
r, w, _ := os.Pipe()
os.Stdout = w
err := execute(rooCmd, args, true)
if err != nil {
return
}
// read and restore stdout
if err = w.Close(); err != nil {
t.Fatal(err)
}
out, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
os.Stdout = backupStdout
assert.Equal(t, `root Description for root
Usage: root [command] [flags] [arguments]
Use "root [command] --help" for help on any command.
Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s)
or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s)
Flags:
--fii (Default: "fii")
Fii description
--foo (Default: "foo")
Foo description
--fuu (Default: "test")
Fuu description
--yi (Default: "false")
--yi.fii (Default: "fii")
--yi.foo (Default: "foo")
--yi.fuu (Default: "")
--yu.fii (Default: "fii")
--yu.foo (Default: "foo")
--yu.fuu (Default: "")
`, string(out))
}

50
pkg/cli/file_finder.go Normal file
View file

@ -0,0 +1,50 @@
package cli
import (
"os"
"path/filepath"
"strings"
)
// Finder holds a list of file paths.
type Finder struct {
BasePaths []string
Extensions []string
}
// Find returns the first valid existing file among configFile
// and the paths already registered with Finder.
func (f Finder) Find(configFile string) (string, error) {
paths := f.getPaths(configFile)
for _, filePath := range paths {
fp := os.ExpandEnv(filePath)
_, err := os.Stat(fp)
if os.IsNotExist(err) {
continue
}
if err != nil {
return "", err
}
return filepath.Abs(fp)
}
return "", nil
}
func (f Finder) getPaths(configFile string) []string {
var paths []string
if strings.TrimSpace(configFile) != "" {
paths = append(paths, configFile)
}
for _, basePath := range f.BasePaths {
for _, ext := range f.Extensions {
paths = append(paths, basePath+"."+ext)
}
}
return paths
}

162
pkg/cli/file_finder_test.go Normal file
View file

@ -0,0 +1,162 @@
package cli
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFinder_Find(t *testing.T) {
configFile, err := ioutil.TempFile("", "traefik-file-finder-test-*.toml")
require.NoError(t, err)
defer func() {
_ = os.Remove(configFile.Name())
}()
dir, err := ioutil.TempDir("", "traefik-file-finder-test")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(dir)
}()
fooFile, err := os.Create(filepath.Join(dir, "foo.toml"))
require.NoError(t, err)
_, err = os.Create(filepath.Join(dir, "bar.toml"))
require.NoError(t, err)
type expected struct {
error bool
path string
}
testCases := []struct {
desc string
basePaths []string
configFile string
expected expected
}{
{
desc: "not found: no config file",
configFile: "",
expected: expected{path: ""},
},
{
desc: "not found: no config file, no other paths available",
configFile: "",
basePaths: []string{"/my/path/traefik", "$HOME/my/path/traefik", "./my-traefik"},
expected: expected{path: ""},
},
{
desc: "not found: with non existing config file",
configFile: "/my/path/config.toml",
expected: expected{path: ""},
},
{
desc: "found: with config file",
configFile: configFile.Name(),
expected: expected{path: configFile.Name()},
},
{
desc: "found: no config file, first base path",
configFile: "",
basePaths: []string{filepath.Join(dir, "foo"), filepath.Join(dir, "bar")},
expected: expected{path: fooFile.Name()},
},
{
desc: "found: no config file, base path",
configFile: "",
basePaths: []string{"/my/path/traefik", "$HOME/my/path/traefik", filepath.Join(dir, "foo")},
expected: expected{path: fooFile.Name()},
},
{
desc: "found: config file over base path",
configFile: configFile.Name(),
basePaths: []string{filepath.Join(dir, "foo"), filepath.Join(dir, "bar")},
expected: expected{path: configFile.Name()},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
finder := Finder{
BasePaths: test.basePaths,
Extensions: []string{"toml", "yaml", "yml"},
}
path, err := finder.Find(test.configFile)
if test.expected.error {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected.path, path)
}
})
}
}
func TestFinder_getPaths(t *testing.T) {
testCases := []struct {
desc string
basePaths []string
configFile string
expected []string
}{
{
desc: "no config file",
basePaths: []string{"/etc/traefik/traefik", "$HOME/.config/traefik", "./traefik"},
configFile: "",
expected: []string{
"/etc/traefik/traefik.toml",
"/etc/traefik/traefik.yaml",
"/etc/traefik/traefik.yml",
"$HOME/.config/traefik.toml",
"$HOME/.config/traefik.yaml",
"$HOME/.config/traefik.yml",
"./traefik.toml",
"./traefik.yaml",
"./traefik.yml",
},
},
{
desc: "with config file",
basePaths: []string{"/etc/traefik/traefik", "$HOME/.config/traefik", "./traefik"},
configFile: "/my/path/config.toml",
expected: []string{
"/my/path/config.toml",
"/etc/traefik/traefik.toml",
"/etc/traefik/traefik.yaml",
"/etc/traefik/traefik.yml",
"$HOME/.config/traefik.toml",
"$HOME/.config/traefik.yaml",
"$HOME/.config/traefik.yml",
"./traefik.toml",
"./traefik.yaml",
"./traefik.yml",
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
finder := Finder{
BasePaths: test.basePaths,
Extensions: []string{"toml", "yaml", "yml"},
}
paths := finder.getPaths(test.configFile)
assert.Equal(t, test.expected, paths)
})
}
}

View file

@ -0,0 +1,3 @@
foo = "bar"
fii = "bir"
[yi]

25
pkg/cli/fixtures_test.go Normal file
View file

@ -0,0 +1,25 @@
package cli
type Yo struct {
Foo string `description:"Foo description"`
Fii string `description:"Fii description"`
Fuu string `description:"Fuu description"`
Yi *Yi `label:"allowEmpty"`
Yu *Yi
}
func (y *Yo) SetDefaults() {
y.Foo = "foo"
y.Fii = "fii"
}
type Yi struct {
Foo string
Fii string
Fuu string
}
func (y *Yi) SetDefaults() {
y.Foo = "foo"
y.Fii = "fii"
}

89
pkg/cli/help.go Normal file
View file

@ -0,0 +1,89 @@
package cli
import (
"io"
"strings"
"text/tabwriter"
"text/template"
"github.com/Masterminds/sprig"
"github.com/containous/traefik/pkg/config/flag"
"github.com/containous/traefik/pkg/config/generator"
"github.com/containous/traefik/pkg/config/parser"
)
const tmplHelp = `{{ .Cmd.Name }} {{ .Cmd.Description }}
Usage: {{ .Cmd.Name }} [command] [flags] [arguments]
Use "{{ .Cmd.Name }} [command] --help" for help on any command.
{{if .SubCommands }}
Commands:
{{- range $i, $subCmd := .SubCommands }}
{{ if not $subCmd.Hidden }} {{ $subCmd.Name }} {{ $subCmd.Description }}{{end}}{{end}}
{{end}}
{{- if .Flags }}
Flag's usage: {{ .Cmd.Name }} [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s)
or: {{ .Cmd.Name }} [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s)
Flags:
{{- range $i, $flag := .Flags }}
--{{ SliceIndexN $flag.Name }} {{if ne $flag.Name "global.sendanonymoususage"}}(Default: "{{ $flag.Default}}"){{end}}
{{if $flag.Description }} {{ wrapWith 80 "\n\t\t" $flag.Description }}
{{else}}
{{- end}}
{{- end}}
{{- end}}
`
func isHelp(args []string) bool {
for _, name := range args {
if name == "--help" || name == "-help" || name == "-h" {
return true
}
}
return false
}
// PrintHelp prints the help for the command given as argument.
func PrintHelp(w io.Writer, cmd *Command) error {
var flags []parser.Flat
if cmd.Configuration != nil {
generator.Generate(cmd.Configuration)
var err error
flags, err = flag.Encode(cmd.Configuration)
if err != nil {
return err
}
}
model := map[string]interface{}{
"Cmd": cmd,
"Flags": flags,
"SubCommands": cmd.subCommands,
}
funcs := sprig.TxtFuncMap()
funcs["SliceIndexN"] = sliceIndexN
tmpl, err := template.New("flags").
Funcs(funcs).
Parse(tmplHelp)
if err != nil {
return err
}
tw := tabwriter.NewWriter(w, 4, 0, 4, ' ', 0)
err = tmpl.Execute(tw, model)
if err != nil {
return err
}
return tw.Flush()
}
func sliceIndexN(flag string) string {
return strings.ReplaceAll(flag, "[0]", "[n]")
}

211
pkg/cli/help_test.go Normal file
View file

@ -0,0 +1,211 @@
package cli
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrintHelp(t *testing.T) {
testCases := []struct {
desc string
command *Command
expected string
}{
{
desc: "no sub-command, with flags",
command: func() *Command {
element := &Yo{
Fuu: "test",
}
return &Command{
Name: "root",
Description: "Description for root",
Configuration: element,
Run: func(args []string) error {
return nil
},
}
}(),
expected: `root Description for root
Usage: root [command] [flags] [arguments]
Use "root [command] --help" for help on any command.
Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s)
or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s)
Flags:
--fii (Default: "fii")
Fii description
--foo (Default: "foo")
Foo description
--fuu (Default: "test")
Fuu description
--yi (Default: "false")
--yi.fii (Default: "fii")
--yi.foo (Default: "foo")
--yi.fuu (Default: "")
--yu.fii (Default: "fii")
--yu.foo (Default: "foo")
--yu.fuu (Default: "")
`,
},
{
desc: "with sub-commands, with flags, call root help",
command: func() *Command {
element := &Yo{
Fuu: "test",
}
rootCmd := &Command{
Name: "root",
Description: "Description for root",
Configuration: element,
Run: func(_ []string) error {
return nil
},
}
err := rootCmd.AddCommand(&Command{
Name: "sub1",
Description: "Description for sub1",
Configuration: element,
Run: func(args []string) error {
return nil
},
})
require.NoError(t, err)
err = rootCmd.AddCommand(&Command{
Name: "sub2",
Description: "Description for sub2",
Configuration: element,
Run: func(args []string) error {
return nil
},
})
require.NoError(t, err)
return rootCmd
}(),
expected: `root Description for root
Usage: root [command] [flags] [arguments]
Use "root [command] --help" for help on any command.
Commands:
sub1 Description for sub1
sub2 Description for sub2
Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s)
or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s)
Flags:
--fii (Default: "fii")
Fii description
--foo (Default: "foo")
Foo description
--fuu (Default: "test")
Fuu description
--yi (Default: "false")
--yi.fii (Default: "fii")
--yi.foo (Default: "foo")
--yi.fuu (Default: "")
--yu.fii (Default: "fii")
--yu.foo (Default: "foo")
--yu.fuu (Default: "")
`,
},
{
desc: "no sub-command, no flags",
command: func() *Command {
return &Command{
Name: "root",
Description: "Description for root",
Configuration: nil,
Run: func(args []string) error {
return nil
},
}
}(),
expected: `root Description for root
Usage: root [command] [flags] [arguments]
Use "root [command] --help" for help on any command.
`,
},
{
desc: "no sub-command, slice flags",
command: func() *Command {
return &Command{
Name: "root",
Description: "Description for root",
Configuration: &struct {
Foo []struct {
Field string
}
}{},
Run: func(args []string) error {
return nil
},
}
}(),
expected: `root Description for root
Usage: root [command] [flags] [arguments]
Use "root [command] --help" for help on any command.
Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s)
or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s)
Flags:
--foo (Default: "")
--foo[n].field (Default: "")
`,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
buffer := &bytes.Buffer{}
err := PrintHelp(buffer, test.command)
require.NoError(t, err)
assert.Equal(t, test.expected, buffer.String())
})
}
}

21
pkg/cli/loader.go Normal file
View file

@ -0,0 +1,21 @@
package cli
// ResourceLoader is a configuration resource loader.
type ResourceLoader interface {
// Load populates cmd.Configuration, optionally using args to do so.
Load(args []string, cmd *Command) (bool, error)
}
type filenameGetter interface {
GetFilename() string
}
// GetConfigFile returns the configuration file corresponding to the first configuration file loader found in ResourceLoader, if any.
func GetConfigFile(loaders []ResourceLoader) string {
for _, loader := range loaders {
if v, ok := loader.(filenameGetter); ok {
return v.GetFilename()
}
}
return ""
}

40
pkg/cli/loader_env.go Normal file
View file

@ -0,0 +1,40 @@
package cli
import (
"fmt"
"os"
"strings"
"github.com/containous/traefik/pkg/config/env"
"github.com/containous/traefik/pkg/log"
)
// EnvLoader loads a configuration from all the environment variables prefixed with "TRAEFIK_".
type EnvLoader struct{}
// Load loads the command's configuration from the environment variables.
func (e *EnvLoader) Load(_ []string, cmd *Command) (bool, error) {
return e.load(os.Environ(), cmd)
}
func (*EnvLoader) load(environ []string, cmd *Command) (bool, error) {
var found bool
for _, value := range environ {
if strings.HasPrefix(value, "TRAEFIK_") {
found = true
break
}
}
if !found {
return false, nil
}
if err := env.Decode(environ, cmd.Configuration); err != nil {
return false, fmt.Errorf("failed to decode configuration from environment variables: %v", err)
}
log.WithoutContext().Println("Configuration loaded from environment variables.")
return true, nil
}

78
pkg/cli/loader_file.go Normal file
View file

@ -0,0 +1,78 @@
package cli
import (
"io/ioutil"
"os"
"strings"
"github.com/containous/traefik/pkg/config/file"
"github.com/containous/traefik/pkg/config/flag"
"github.com/containous/traefik/pkg/log"
)
// FileLoader loads a configuration from a file.
type FileLoader struct {
ConfigFileFlag string
filename string
}
// GetFilename returns the configuration file if any.
func (f *FileLoader) GetFilename() string {
return f.filename
}
// Load loads the command's configuration from a file either specified with the -traefik.configfile flag, or from default locations.
func (f *FileLoader) Load(args []string, cmd *Command) (bool, error) {
ref, err := flag.Parse(args, cmd.Configuration)
if err != nil {
_ = PrintHelp(os.Stdout, cmd)
return false, err
}
configFileFlag := "traefik.configfile"
if f.ConfigFileFlag != "" {
configFileFlag = "traefik." + strings.ToLower(f.ConfigFileFlag)
}
configFile, err := loadConfigFiles(ref[configFileFlag], cmd.Configuration)
if err != nil {
return false, err
}
f.filename = configFile
if configFile == "" {
return false, nil
}
logger := log.WithoutContext()
logger.Printf("Configuration loaded from file: %s", configFile)
content, _ := ioutil.ReadFile(configFile)
logger.Debug(string(content))
return true, nil
}
// loadConfigFiles tries to decode the given configuration file and all default locations for the configuration file.
// It stops as soon as decoding one of them is successful.
func loadConfigFiles(configFile string, element interface{}) (string, error) {
finder := Finder{
BasePaths: []string{"/etc/traefik/traefik", "$XDG_CONFIG_HOME/traefik", "$HOME/.config/traefik", "./traefik"},
Extensions: []string{"toml", "yaml", "yml"},
}
filePath, err := finder.Find(configFile)
if err != nil {
return "", err
}
if len(filePath) == 0 {
return "", nil
}
if err = file.Decode(filePath, element); err != nil {
return "", err
}
return filePath, nil
}

22
pkg/cli/loader_flag.go Normal file
View file

@ -0,0 +1,22 @@
package cli
import (
"fmt"
"github.com/containous/traefik/pkg/config/flag"
"github.com/containous/traefik/pkg/log"
)
// FlagLoader loads configuration from flags.
type FlagLoader struct{}
// Load loads the command's configuration from flag arguments.
func (*FlagLoader) Load(args []string, cmd *Command) (bool, error) {
if err := flag.Decode(args, cmd.Configuration); err != nil {
return false, fmt.Errorf("failed to decode configuration from flags: %v", err)
}
log.WithoutContext().Println("Configuration loaded from flags.")
return true, nil
}

View file

@ -56,7 +56,11 @@ func Collect(staticConfiguration *static.Configuration) error {
return err return err
} }
_, err = makeHTTPClient().Post(collectorURL, "application/json; charset=utf-8", buf) resp, err := makeHTTPClient().Post(collectorURL, "application/json; charset=utf-8", buf)
if resp != nil {
resp.Body.Close()
}
return err return err
} }

View file

@ -125,9 +125,9 @@ type HealthCheck struct {
Scheme string `json:"scheme,omitempty" toml:",omitempty"` Scheme string `json:"scheme,omitempty" toml:",omitempty"`
Path string `json:"path,omitempty" toml:",omitempty"` Path string `json:"path,omitempty" toml:",omitempty"`
Port int `json:"port,omitempty" toml:",omitempty,omitzero"` Port int `json:"port,omitempty" toml:",omitempty,omitzero"`
// FIXME change string to parse.Duration // FIXME change string to types.Duration
Interval string `json:"interval,omitempty" toml:",omitempty"` Interval string `json:"interval,omitempty" toml:",omitempty"`
// FIXME change string to parse.Duration // FIXME change string to types.Duration
Timeout string `json:"timeout,omitempty" toml:",omitempty"` Timeout string `json:"timeout,omitempty" toml:",omitempty"`
Hostname string `json:"hostname,omitempty" toml:",omitempty"` Hostname string `json:"hostname,omitempty" toml:",omitempty"`
Headers map[string]string `json:"headers,omitempty" toml:",omitempty"` Headers map[string]string `json:"headers,omitempty" toml:",omitempty"`

50
pkg/config/env/env.go vendored Normal file
View file

@ -0,0 +1,50 @@
// Package env implements encoding and decoding between environment variable and a typed Configuration.
package env
import (
"strings"
"github.com/containous/traefik/pkg/config/parser"
)
// Decode decodes the given environment variables into the given element.
// The operation goes through four stages roughly summarized as:
// env vars -> map
// map -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element
func Decode(environ []string, element interface{}) error {
vars := make(map[string]string)
for _, evr := range environ {
n := strings.SplitN(evr, "=", 2)
if strings.HasPrefix(strings.ToUpper(n[0]), "TRAEFIK_") {
key := strings.ReplaceAll(strings.ToLower(n[0]), "_", ".")
vars[key] = n[1]
}
}
return parser.Decode(vars, element)
}
// Encode encodes the configuration in element into the environment variables represented in the returned Flats.
// The operation goes through three stages roughly summarized as:
// typed configuration in element -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> environment variables with default values (determined by type/kind)
func Encode(element interface{}) ([]parser.Flat, error) {
if element == nil {
return nil, nil
}
node, err := parser.EncodeToNode(element, false)
if err != nil {
return nil, err
}
err = parser.AddMetadata(element, node)
if err != nil {
return nil, err
}
return parser.EncodeToFlat(element, node, parser.FlatOpts{Case: "upper", Separator: "_"})
}

498
pkg/config/env/env_test.go vendored Normal file
View file

@ -0,0 +1,498 @@
package env
import (
"testing"
"github.com/containous/traefik/pkg/config/generator"
"github.com/containous/traefik/pkg/config/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecode(t *testing.T) {
testCases := []struct {
desc string
environ []string
element interface{}
expected interface{}
}{
{
desc: "no env vars",
environ: nil,
expected: nil,
},
{
desc: "bool value",
environ: []string{"TRAEFIK_FOO=true"},
element: &struct {
Foo bool
}{},
expected: &struct {
Foo bool
}{
Foo: true,
},
},
{
desc: "equal",
environ: []string{"TRAEFIK_FOO=bar"},
element: &struct {
Foo string
}{},
expected: &struct {
Foo string
}{
Foo: "bar",
},
},
{
desc: "multiple bool flags without value",
environ: []string{"TRAEFIK_FOO=true", "TRAEFIK_BAR=true"},
element: &struct {
Foo bool
Bar bool
}{},
expected: &struct {
Foo bool
Bar bool
}{
Foo: true,
Bar: true,
},
},
{
desc: "map string",
environ: []string{"TRAEFIK_FOO_NAME=bar"},
element: &struct {
Foo map[string]string
}{},
expected: &struct {
Foo map[string]string
}{
Foo: map[string]string{
"name": "bar",
},
},
},
{
desc: "map struct",
environ: []string{"TRAEFIK_FOO_NAME_VALUE=bar"},
element: &struct {
Foo map[string]struct{ Value string }
}{},
expected: &struct {
Foo map[string]struct{ Value string }
}{
Foo: map[string]struct{ Value string }{
"name": {
Value: "bar",
},
},
},
},
{
desc: "map struct with sub-struct",
environ: []string{"TRAEFIK_FOO_NAME_BAR_VALUE=bar"},
element: &struct {
Foo map[string]struct {
Bar *struct{ Value string }
}
}{},
expected: &struct {
Foo map[string]struct {
Bar *struct{ Value string }
}
}{
Foo: map[string]struct {
Bar *struct{ Value string }
}{
"name": {
Bar: &struct {
Value string
}{
Value: "bar",
},
},
},
},
},
{
desc: "map struct with sub-map",
environ: []string{"TRAEFIK_FOO_NAME1_BAR_NAME2_VALUE=bar"},
element: &struct {
Foo map[string]struct {
Bar map[string]struct{ Value string }
}
}{},
expected: &struct {
Foo map[string]struct {
Bar map[string]struct{ Value string }
}
}{
Foo: map[string]struct {
Bar map[string]struct{ Value string }
}{
"name1": {
Bar: map[string]struct{ Value string }{
"name2": {
Value: "bar",
},
},
},
},
},
},
{
desc: "slice",
environ: []string{"TRAEFIK_FOO=bar,baz"},
element: &struct {
Foo []string
}{},
expected: &struct {
Foo []string
}{
Foo: []string{"bar", "baz"},
},
},
{
desc: "struct pointer value",
environ: []string{"TRAEFIK_FOO=true"},
element: &struct {
Foo *struct{ Field string } `label:"allowEmpty"`
}{},
expected: &struct {
Foo *struct{ Field string } `label:"allowEmpty"`
}{
Foo: &struct{ Field string }{},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := Decode(test.environ, test.element)
require.NoError(t, err)
assert.Equal(t, test.expected, test.element)
})
}
}
func TestEncode(t *testing.T) {
element := &Ya{
Foo: &Yaa{
FieldIn1: "bar",
FieldIn2: false,
FieldIn3: 1,
FieldIn4: map[string]string{
parser.MapNamePlaceholder: "",
},
FieldIn5: map[string]int{
parser.MapNamePlaceholder: 0,
},
FieldIn6: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
FieldIn7: map[string]struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
FieldIn8: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {},
},
FieldIn9: map[string]*struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
FieldIn10: struct{ Field string }{},
FieldIn11: &struct{ Field string }{},
FieldIn12: func(v string) *string { return &v }(""),
FieldIn13: func(v bool) *bool { return &v }(false),
FieldIn14: func(v int) *int { return &v }(0),
},
Field1: "bir",
Field2: true,
Field3: 0,
Field4: map[string]string{
parser.MapNamePlaceholder: "",
},
Field5: map[string]int{
parser.MapNamePlaceholder: 0,
},
Field6: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
Field7: map[string]struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
Field8: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {},
},
Field9: map[string]*struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
Field10: struct{ Field string }{},
Field11: &struct{ Field string }{},
Field12: func(v string) *string { return &v }(""),
Field13: func(v bool) *bool { return &v }(false),
Field14: func(v int) *int { return &v }(0),
Field15: []int{7},
}
generator.Generate(element)
flats, err := Encode(element)
require.NoError(t, err)
expected := []parser.Flat{
{
Name: "TRAEFIK_FIELD1",
Description: "",
Default: "bir",
},
{
Name: "TRAEFIK_FIELD10",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD10_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD11_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD12",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD13",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FIELD14",
Description: "",
Default: "0",
},
{
Name: "TRAEFIK_FIELD15",
Description: "",
Default: "7",
},
{
Name: "TRAEFIK_FIELD2",
Description: "",
Default: "true",
},
{
Name: "TRAEFIK_FIELD3",
Description: "",
Default: "0",
},
{
Name: "TRAEFIK_FIELD4_\u003cNAME\u003e",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD5_\u003cNAME\u003e",
Description: "",
Default: "0",
},
{
Name: "TRAEFIK_FIELD6_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FIELD6_\u003cNAME\u003e_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD7_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FIELD7_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD8_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FIELD8_\u003cNAME\u003e_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FIELD9_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FIELD9_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN1",
Description: "",
Default: "bar",
},
{
Name: "TRAEFIK_FOO_FIELDIN10",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN10_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN11_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN12",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN13",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FOO_FIELDIN14",
Description: "",
Default: "0",
},
{
Name: "TRAEFIK_FOO_FIELDIN2",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FOO_FIELDIN3",
Description: "",
Default: "1",
},
{
Name: "TRAEFIK_FOO_FIELDIN4_\u003cNAME\u003e",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN5_\u003cNAME\u003e",
Description: "",
Default: "0",
},
{
Name: "TRAEFIK_FOO_FIELDIN6_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FOO_FIELDIN6_\u003cNAME\u003e_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN7_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FOO_FIELDIN7_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN8_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FOO_FIELDIN8_\u003cNAME\u003e_FIELD",
Description: "",
Default: "",
},
{
Name: "TRAEFIK_FOO_FIELDIN9_\u003cNAME\u003e",
Description: "",
Default: "false",
},
{
Name: "TRAEFIK_FOO_FIELDIN9_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
Description: "",
Default: "",
},
}
assert.Equal(t, expected, flats)
}
type Ya struct {
Foo *Yaa
Field1 string
Field2 bool
Field3 int
Field4 map[string]string
Field5 map[string]int
Field6 map[string]struct{ Field string }
Field7 map[string]struct{ Field map[string]string }
Field8 map[string]*struct{ Field string }
Field9 map[string]*struct{ Field map[string]string }
Field10 struct{ Field string }
Field11 *struct{ Field string }
Field12 *string
Field13 *bool
Field14 *int
Field15 []int
}
type Yaa struct {
FieldIn1 string
FieldIn2 bool
FieldIn3 int
FieldIn4 map[string]string
FieldIn5 map[string]int
FieldIn6 map[string]struct{ Field string }
FieldIn7 map[string]struct{ Field map[string]string }
FieldIn8 map[string]*struct{ Field string }
FieldIn9 map[string]*struct{ Field map[string]string }
FieldIn10 struct{ Field string }
FieldIn11 *struct{ Field string }
FieldIn12 *string
FieldIn13 *bool
FieldIn14 *int
}

31
pkg/config/file/file.go Normal file
View file

@ -0,0 +1,31 @@
// Package file implements decoding between configuration in a file and a typed Configuration.
package file
import (
"github.com/containous/traefik/pkg/config/parser"
)
// Decode decodes the given configuration file into the given element.
// The operation goes through three stages roughly summarized as:
// file contents -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element
func Decode(filePath string, element interface{}) error {
if element == nil {
return nil
}
filters := getRootFieldNames(element)
root, err := decodeFileToNode(filePath, filters...)
if err != nil {
return err
}
err = parser.AddMetadata(element, root)
if err != nil {
return err
}
return parser.Fill(element, root)
}

View file

@ -0,0 +1,86 @@
package file
import (
"fmt"
"io/ioutil"
"path/filepath"
"reflect"
"github.com/BurntSushi/toml"
"github.com/containous/traefik/pkg/config/parser"
"gopkg.in/yaml.v2"
)
// decodeFileToNode decodes the configuration in filePath in a tree of untyped nodes.
// If filters is not empty, it skips any configuration element whose name is
// not among filters.
func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
data := make(map[string]interface{})
switch filepath.Ext(filePath) {
case ".toml":
err = toml.Unmarshal(content, &data)
if err != nil {
return nil, err
}
case ".yml", ".yaml":
var err error
err = yaml.Unmarshal(content, data)
if err != nil {
return nil, err
}
return decodeRawToNode(data, filters...)
default:
return nil, fmt.Errorf("unsupported file extension: %s", filePath)
}
return decodeRawToNode(data, filters...)
}
func getRootFieldNames(element interface{}) []string {
if element == nil {
return nil
}
rootType := reflect.TypeOf(element)
return getFieldNames(rootType)
}
func getFieldNames(rootType reflect.Type) []string {
var names []string
if rootType.Kind() == reflect.Ptr {
rootType = rootType.Elem()
}
if rootType.Kind() != reflect.Struct {
return nil
}
for i := 0; i < rootType.NumField(); i++ {
field := rootType.Field(i)
if !parser.IsExported(field) {
continue
}
if field.Anonymous &&
(field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) {
names = append(names, getFieldNames(field.Type)...)
continue
}
names = append(names, field.Name)
}
return names
}

View file

@ -0,0 +1,599 @@
package file
import (
"testing"
"github.com/containous/traefik/pkg/config/parser"
"github.com/stretchr/testify/assert"
)
func Test_getRootFieldNames(t *testing.T) {
testCases := []struct {
desc string
element interface{}
expected []string
}{
{
desc: "simple fields",
element: &Yo{},
expected: []string{"Foo", "Fii", "Fuu", "Yi"},
},
{
desc: "embedded struct",
element: &Yu{},
expected: []string{"Foo", "Fii", "Fuu"},
},
{
desc: "embedded struct pointer",
element: &Ye{},
expected: []string{"Foo", "Fii", "Fuu"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
names := getRootFieldNames(test.element)
assert.Equal(t, test.expected, names)
})
}
}
func Test_decodeFileToNode_compare(t *testing.T) {
nodeToml, err := decodeFileToNode("./fixtures/sample.toml",
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "ACME")
if err != nil {
t.Fatal(err)
}
nodeYaml, err := decodeFileToNode("./fixtures/sample.yml")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, nodeToml, nodeYaml)
}
func Test_decodeFileToNode_Toml(t *testing.T) {
node, err := decodeFileToNode("./fixtures/sample.toml",
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "ACME")
if err != nil {
t.Fatal(err)
}
expected := &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "ACME",
Children: []*parser.Node{
{Name: "ACMELogging", Value: "true"},
{Name: "CAServer", Value: "foobar"},
{Name: "DNSChallenge", Children: []*parser.Node{
{Name: "DelayBeforeCheck", Value: "42"},
{Name: "DisablePropagationCheck", Value: "true"},
{Name: "Provider", Value: "foobar"},
{Name: "Resolvers", Value: "foobar,foobar"},
}},
{Name: "Domains", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Main", Value: "foobar"},
{Name: "SANs", Value: "foobar,foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Main", Value: "foobar"},
{Name: "SANs", Value: "foobar,foobar"},
}},
}},
{Name: "Email", Value: "foobar"},
{Name: "EntryPoint", Value: "foobar"},
{Name: "HTTPChallenge", Children: []*parser.Node{
{Name: "EntryPoint", Value: "foobar"}}},
{Name: "KeyType", Value: "foobar"},
{Name: "OnHostRule", Value: "true"},
{Name: "Storage", Value: "foobar"},
{Name: "TLSChallenge"},
},
},
{Name: "API", Children: []*parser.Node{
{Name: "Dashboard", Value: "true"},
{Name: "EntryPoint", Value: "foobar"},
{Name: "Middlewares", Value: "foobar,foobar"},
{Name: "Statistics", Children: []*parser.Node{
{Name: "RecentErrors", Value: "42"}}}}},
{Name: "AccessLog", Children: []*parser.Node{
{Name: "BufferingSize", Value: "42"},
{Name: "Fields", Children: []*parser.Node{
{Name: "DefaultMode", Value: "foobar"},
{Name: "Headers", Children: []*parser.Node{
{Name: "DefaultMode", Value: "foobar"},
{Name: "Names", Children: []*parser.Node{
{Name: "name0", Value: "foobar"},
{Name: "name1", Value: "foobar"}}}}},
{Name: "Names", Children: []*parser.Node{
{Name: "name0", Value: "foobar"},
{Name: "name1", Value: "foobar"}}}}},
{Name: "FilePath", Value: "foobar"},
{Name: "Filters", Children: []*parser.Node{
{Name: "MinDuration", Value: "42"},
{Name: "RetryAttempts", Value: "true"},
{Name: "StatusCodes", Value: "foobar,foobar"}}},
{Name: "Format", Value: "foobar"}}},
{Name: "EntryPoints", Children: []*parser.Node{
{Name: "EntryPoint0", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "ForwardedHeaders", Children: []*parser.Node{
{Name: "Insecure", Value: "true"},
{Name: "TrustedIPs", Value: "foobar,foobar"}}},
{Name: "ProxyProtocol", Children: []*parser.Node{
{Name: "Insecure", Value: "true"},
{Name: "TrustedIPs", Value: "foobar,foobar"}}},
{Name: "Transport", Children: []*parser.Node{
{Name: "LifeCycle", Children: []*parser.Node{
{Name: "GraceTimeOut", Value: "42"},
{Name: "RequestAcceptGraceTimeout", Value: "42"}}},
{Name: "RespondingTimeouts", Children: []*parser.Node{
{Name: "IdleTimeout", Value: "42"},
{Name: "ReadTimeout", Value: "42"},
{Name: "WriteTimeout", Value: "42"}}}}}}}}},
{Name: "Global", Children: []*parser.Node{
{Name: "CheckNewVersion", Value: "true"},
{Name: "Debug", Value: "true"},
{Name: "SendAnonymousUsage", Value: "true"}}},
{Name: "HostResolver", Children: []*parser.Node{
{Name: "CnameFlattening", Value: "true"},
{Name: "ResolvConfig", Value: "foobar"},
{Name: "ResolvDepth", Value: "42"}}},
{Name: "Log", Children: []*parser.Node{
{Name: "FilePath", Value: "foobar"},
{Name: "Format", Value: "foobar"},
{Name: "Level", Value: "foobar"}}},
{Name: "Metrics", Children: []*parser.Node{
{Name: "Datadog", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "PushInterval", Value: "10s"}}},
{Name: "InfluxDB", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "Database", Value: "foobar"},
{Name: "Password", Value: "foobar"},
{Name: "Protocol", Value: "foobar"},
{Name: "PushInterval", Value: "10s"},
{Name: "RetentionPolicy", Value: "foobar"},
{Name: "Username", Value: "foobar"}}},
{Name: "Prometheus", Children: []*parser.Node{
{Name: "Buckets", Value: "42,42"},
{Name: "EntryPoint", Value: "foobar"},
{Name: "Middlewares", Value: "foobar,foobar"}}},
{Name: "StatsD", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "PushInterval", Value: "10s"}}}}},
{Name: "Ping", Children: []*parser.Node{
{Name: "EntryPoint", Value: "foobar"},
{Name: "Middlewares", Value: "foobar,foobar"}}},
{Name: "Providers", Children: []*parser.Node{
{Name: "Docker", Children: []*parser.Node{
{Name: "Constraints", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
}},
{Name: "DefaultRule", Value: "foobar"},
{Name: "Endpoint", Value: "foobar"},
{Name: "ExposedByDefault", Value: "true"},
{Name: "Network", Value: "foobar"},
{Name: "SwarmMode", Value: "true"},
{Name: "SwarmModeRefreshSeconds", Value: "42"},
{Name: "TLS", Children: []*parser.Node{
{Name: "CA", Value: "foobar"},
{Name: "CAOptional", Value: "true"},
{Name: "Cert", Value: "foobar"},
{Name: "InsecureSkipVerify", Value: "true"},
{Name: "Key", Value: "foobar"}}},
{Name: "UseBindPortIP", Value: "true"},
{Name: "Watch", Value: "true"}}},
{Name: "File", Children: []*parser.Node{
{Name: "DebugLogGeneratedTemplate", Value: "true"},
{Name: "Directory", Value: "foobar"},
{Name: "Filename", Value: "foobar"},
{Name: "TraefikFile", Value: "foobar"},
{Name: "Watch", Value: "true"}}},
{Name: "Kubernetes", Children: []*parser.Node{
{Name: "CertAuthFilePath", Value: "foobar"},
{Name: "DisablePassHostHeaders", Value: "true"},
{Name: "Endpoint", Value: "foobar"},
{Name: "IngressClass", Value: "foobar"},
{Name: "IngressEndpoint", Children: []*parser.Node{
{Name: "Hostname", Value: "foobar"},
{Name: "IP", Value: "foobar"},
{Name: "PublishedService", Value: "foobar"}}},
{Name: "LabelSelector", Value: "foobar"},
{Name: "Namespaces", Value: "foobar,foobar"},
{Name: "Token", Value: "foobar"}}},
{Name: "KubernetesCRD",
Children: []*parser.Node{
{Name: "CertAuthFilePath", Value: "foobar"},
{Name: "DisablePassHostHeaders", Value: "true"},
{Name: "Endpoint", Value: "foobar"},
{Name: "IngressClass", Value: "foobar"},
{Name: "LabelSelector", Value: "foobar"},
{Name: "Namespaces", Value: "foobar,foobar"},
{Name: "Token", Value: "foobar"}}},
{Name: "Marathon", Children: []*parser.Node{
{Name: "Basic", Children: []*parser.Node{
{Name: "HTTPBasicAuthUser", Value: "foobar"},
{Name: "HTTPBasicPassword", Value: "foobar"}}},
{Name: "Constraints", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
}},
{Name: "DCOSToken", Value: "foobar"},
{Name: "DefaultRule", Value: "foobar"},
{Name: "DialerTimeout", Value: "42"},
{Name: "Endpoint", Value: "foobar"},
{Name: "ExposedByDefault", Value: "true"},
{Name: "FilterMarathonConstraints", Value: "true"},
{Name: "ForceTaskHostname", Value: "true"},
{Name: "KeepAlive", Value: "42"},
{Name: "RespectReadinessChecks", Value: "true"},
{Name: "ResponseHeaderTimeout", Value: "42"},
{Name: "TLS", Children: []*parser.Node{
{Name: "CA", Value: "foobar"},
{Name: "CAOptional", Value: "true"},
{Name: "Cert", Value: "foobar"},
{Name: "InsecureSkipVerify", Value: "true"},
{Name: "Key", Value: "foobar"}}},
{Name: "TLSHandshakeTimeout", Value: "42"},
{Name: "Trace", Value: "true"},
{Name: "Watch", Value: "true"}}},
{Name: "ProvidersThrottleDuration", Value: "42"},
{Name: "Rancher", Children: []*parser.Node{
{Name: "Constraints", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
}},
{Name: "DefaultRule", Value: "foobar"},
{Name: "EnableServiceHealthFilter", Value: "true"},
{Name: "ExposedByDefault", Value: "true"},
{Name: "IntervalPoll", Value: "true"},
{Name: "Prefix", Value: "foobar"},
{Name: "RefreshSeconds", Value: "42"},
{Name: "Watch", Value: "true"}}},
{Name: "Rest", Children: []*parser.Node{
{Name: "EntryPoint", Value: "foobar"}}}}},
{Name: "ServersTransport", Children: []*parser.Node{
{Name: "ForwardingTimeouts", Children: []*parser.Node{
{Name: "DialTimeout", Value: "42"},
{Name: "ResponseHeaderTimeout", Value: "42"}}},
{Name: "InsecureSkipVerify", Value: "true"},
{Name: "MaxIdleConnsPerHost", Value: "42"},
{Name: "RootCAs", Value: "foobar,foobar"}}},
{Name: "Tracing", Children: []*parser.Node{
{Name: "Backend", Value: "foobar"},
{Name: "DataDog", Children: []*parser.Node{
{Name: "BagagePrefixHeaderName", Value: "foobar"},
{Name: "Debug", Value: "true"},
{Name: "GlobalTag", Value: "foobar"},
{Name: "LocalAgentHostPort", Value: "foobar"},
{Name: "ParentIDHeaderName", Value: "foobar"},
{Name: "PrioritySampling", Value: "true"},
{Name: "SamplingPriorityHeaderName", Value: "foobar"},
{Name: "TraceIDHeaderName", Value: "foobar"}}},
{Name: "Instana", Children: []*parser.Node{
{Name: "LocalAgentHost", Value: "foobar"},
{Name: "LocalAgentPort", Value: "42"},
{Name: "LogLevel", Value: "foobar"}}},
{Name: "Jaeger", Children: []*parser.Node{
{Name: "Gen128Bit", Value: "true"},
{Name: "LocalAgentHostPort", Value: "foobar"},
{Name: "Propagation", Value: "foobar"},
{Name: "SamplingParam", Value: "42"},
{Name: "SamplingServerURL", Value: "foobar"},
{Name: "SamplingType", Value: "foobar"},
{Name: "TraceContextHeaderName", Value: "foobar"}}},
{Name: "ServiceName", Value: "foobar"},
{Name: "SpanNameLimit", Value: "42"},
{Name: "Zipkin", Children: []*parser.Node{
{Name: "Debug", Value: "true"},
{Name: "HTTPEndpoint", Value: "foobar"},
{Name: "ID128Bit", Value: "true"},
{Name: "SameSpan", Value: "true"},
{Name: "SampleRate", Value: "42"}}}}}},
}
assert.Equal(t, expected, node)
}
func Test_decodeFileToNode_Yaml(t *testing.T) {
node, err := decodeFileToNode("./fixtures/sample.yml")
if err != nil {
t.Fatal(err)
}
expected := &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "ACME",
Children: []*parser.Node{
{Name: "ACMELogging", Value: "true"},
{Name: "CAServer", Value: "foobar"},
{Name: "DNSChallenge", Children: []*parser.Node{
{Name: "DelayBeforeCheck", Value: "42"},
{Name: "DisablePropagationCheck", Value: "true"},
{Name: "Provider", Value: "foobar"},
{Name: "Resolvers", Value: "foobar,foobar"},
}},
{Name: "Domains", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Main", Value: "foobar"},
{Name: "SANs", Value: "foobar,foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Main", Value: "foobar"},
{Name: "SANs", Value: "foobar,foobar"},
}},
}},
{Name: "Email", Value: "foobar"},
{Name: "EntryPoint", Value: "foobar"},
{Name: "HTTPChallenge", Children: []*parser.Node{
{Name: "EntryPoint", Value: "foobar"}}},
{Name: "KeyType", Value: "foobar"},
{Name: "OnHostRule", Value: "true"},
{Name: "Storage", Value: "foobar"},
{Name: "TLSChallenge"},
},
},
{Name: "API", Children: []*parser.Node{
{Name: "Dashboard", Value: "true"},
{Name: "EntryPoint", Value: "foobar"},
{Name: "Middlewares", Value: "foobar,foobar"},
{Name: "Statistics", Children: []*parser.Node{
{Name: "RecentErrors", Value: "42"}}}}},
{Name: "AccessLog", Children: []*parser.Node{
{Name: "BufferingSize", Value: "42"},
{Name: "Fields", Children: []*parser.Node{
{Name: "DefaultMode", Value: "foobar"},
{Name: "Headers", Children: []*parser.Node{
{Name: "DefaultMode", Value: "foobar"},
{Name: "Names", Children: []*parser.Node{
{Name: "name0", Value: "foobar"},
{Name: "name1", Value: "foobar"}}}}},
{Name: "Names", Children: []*parser.Node{
{Name: "name0", Value: "foobar"},
{Name: "name1", Value: "foobar"}}}}},
{Name: "FilePath", Value: "foobar"},
{Name: "Filters", Children: []*parser.Node{
{Name: "MinDuration", Value: "42"},
{Name: "RetryAttempts", Value: "true"},
{Name: "StatusCodes", Value: "foobar,foobar"}}},
{Name: "Format", Value: "foobar"}}},
{Name: "EntryPoints", Children: []*parser.Node{
{Name: "EntryPoint0", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "ForwardedHeaders", Children: []*parser.Node{
{Name: "Insecure", Value: "true"},
{Name: "TrustedIPs", Value: "foobar,foobar"}}},
{Name: "ProxyProtocol", Children: []*parser.Node{
{Name: "Insecure", Value: "true"},
{Name: "TrustedIPs", Value: "foobar,foobar"}}},
{Name: "Transport", Children: []*parser.Node{
{Name: "LifeCycle", Children: []*parser.Node{
{Name: "GraceTimeOut", Value: "42"},
{Name: "RequestAcceptGraceTimeout", Value: "42"}}},
{Name: "RespondingTimeouts", Children: []*parser.Node{
{Name: "IdleTimeout", Value: "42"},
{Name: "ReadTimeout", Value: "42"},
{Name: "WriteTimeout", Value: "42"}}}}}}}}},
{Name: "Global", Children: []*parser.Node{
{Name: "CheckNewVersion", Value: "true"},
{Name: "Debug", Value: "true"},
{Name: "SendAnonymousUsage", Value: "true"}}},
{Name: "HostResolver", Children: []*parser.Node{
{Name: "CnameFlattening", Value: "true"},
{Name: "ResolvConfig", Value: "foobar"},
{Name: "ResolvDepth", Value: "42"}}},
{Name: "Log", Children: []*parser.Node{
{Name: "FilePath", Value: "foobar"},
{Name: "Format", Value: "foobar"},
{Name: "Level", Value: "foobar"}}},
{Name: "Metrics", Children: []*parser.Node{
{Name: "Datadog", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "PushInterval", Value: "10s"}}},
{Name: "InfluxDB", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "Database", Value: "foobar"},
{Name: "Password", Value: "foobar"},
{Name: "Protocol", Value: "foobar"},
{Name: "PushInterval", Value: "10s"},
{Name: "RetentionPolicy", Value: "foobar"},
{Name: "Username", Value: "foobar"}}},
{Name: "Prometheus", Children: []*parser.Node{
{Name: "Buckets", Value: "42,42"},
{Name: "EntryPoint", Value: "foobar"},
{Name: "Middlewares", Value: "foobar,foobar"}}},
{Name: "StatsD", Children: []*parser.Node{
{Name: "Address", Value: "foobar"},
{Name: "PushInterval", Value: "10s"}}}}},
{Name: "Ping", Children: []*parser.Node{
{Name: "EntryPoint", Value: "foobar"},
{Name: "Middlewares", Value: "foobar,foobar"}}},
{Name: "Providers", Children: []*parser.Node{
{Name: "Docker", Children: []*parser.Node{
{Name: "Constraints", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
}},
{Name: "DefaultRule", Value: "foobar"},
{Name: "Endpoint", Value: "foobar"},
{Name: "ExposedByDefault", Value: "true"},
{Name: "Network", Value: "foobar"},
{Name: "SwarmMode", Value: "true"},
{Name: "SwarmModeRefreshSeconds", Value: "42"},
{Name: "TLS", Children: []*parser.Node{
{Name: "CA", Value: "foobar"},
{Name: "CAOptional", Value: "true"},
{Name: "Cert", Value: "foobar"},
{Name: "InsecureSkipVerify", Value: "true"},
{Name: "Key", Value: "foobar"}}},
{Name: "UseBindPortIP", Value: "true"},
{Name: "Watch", Value: "true"}}},
{Name: "File", Children: []*parser.Node{
{Name: "DebugLogGeneratedTemplate", Value: "true"},
{Name: "Directory", Value: "foobar"},
{Name: "Filename", Value: "foobar"},
{Name: "TraefikFile", Value: "foobar"},
{Name: "Watch", Value: "true"}}},
{Name: "Kubernetes", Children: []*parser.Node{
{Name: "CertAuthFilePath", Value: "foobar"},
{Name: "DisablePassHostHeaders", Value: "true"},
{Name: "Endpoint", Value: "foobar"},
{Name: "IngressClass", Value: "foobar"},
{Name: "IngressEndpoint", Children: []*parser.Node{
{Name: "Hostname", Value: "foobar"},
{Name: "IP", Value: "foobar"},
{Name: "PublishedService", Value: "foobar"}}},
{Name: "LabelSelector", Value: "foobar"},
{Name: "Namespaces", Value: "foobar,foobar"},
{Name: "Token", Value: "foobar"}}},
{Name: "KubernetesCRD",
Children: []*parser.Node{
{Name: "CertAuthFilePath", Value: "foobar"},
{Name: "DisablePassHostHeaders", Value: "true"},
{Name: "Endpoint", Value: "foobar"},
{Name: "IngressClass", Value: "foobar"},
{Name: "LabelSelector", Value: "foobar"},
{Name: "Namespaces", Value: "foobar,foobar"},
{Name: "Token", Value: "foobar"}}},
{Name: "Marathon", Children: []*parser.Node{
{Name: "Basic", Children: []*parser.Node{
{Name: "HTTPBasicAuthUser", Value: "foobar"},
{Name: "HTTPBasicPassword", Value: "foobar"}}},
{Name: "Constraints", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
}},
{Name: "DCOSToken", Value: "foobar"},
{Name: "DefaultRule", Value: "foobar"},
{Name: "DialerTimeout", Value: "42"},
{Name: "Endpoint", Value: "foobar"},
{Name: "ExposedByDefault", Value: "true"},
{Name: "FilterMarathonConstraints", Value: "true"},
{Name: "ForceTaskHostname", Value: "true"},
{Name: "KeepAlive", Value: "42"},
{Name: "RespectReadinessChecks", Value: "true"},
{Name: "ResponseHeaderTimeout", Value: "42"},
{Name: "TLS", Children: []*parser.Node{
{Name: "CA", Value: "foobar"},
{Name: "CAOptional", Value: "true"},
{Name: "Cert", Value: "foobar"},
{Name: "InsecureSkipVerify", Value: "true"},
{Name: "Key", Value: "foobar"}}},
{Name: "TLSHandshakeTimeout", Value: "42"},
{Name: "Trace", Value: "true"},
{Name: "Watch", Value: "true"}}},
{Name: "ProvidersThrottleDuration", Value: "42"},
{Name: "Rancher", Children: []*parser.Node{
{Name: "Constraints", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "Key", Value: "foobar"},
{Name: "MustMatch", Value: "true"},
{Name: "Value", Value: "foobar"},
}},
}},
{Name: "DefaultRule", Value: "foobar"},
{Name: "EnableServiceHealthFilter", Value: "true"},
{Name: "ExposedByDefault", Value: "true"},
{Name: "IntervalPoll", Value: "true"},
{Name: "Prefix", Value: "foobar"},
{Name: "RefreshSeconds", Value: "42"},
{Name: "Watch", Value: "true"}}},
{Name: "Rest", Children: []*parser.Node{
{Name: "EntryPoint", Value: "foobar"}}}}},
{Name: "ServersTransport", Children: []*parser.Node{
{Name: "ForwardingTimeouts", Children: []*parser.Node{
{Name: "DialTimeout", Value: "42"},
{Name: "ResponseHeaderTimeout", Value: "42"}}},
{Name: "InsecureSkipVerify", Value: "true"},
{Name: "MaxIdleConnsPerHost", Value: "42"},
{Name: "RootCAs", Value: "foobar,foobar"}}},
{Name: "Tracing", Children: []*parser.Node{
{Name: "Backend", Value: "foobar"},
{Name: "DataDog", Children: []*parser.Node{
{Name: "BagagePrefixHeaderName", Value: "foobar"},
{Name: "Debug", Value: "true"},
{Name: "GlobalTag", Value: "foobar"},
{Name: "LocalAgentHostPort", Value: "foobar"},
{Name: "ParentIDHeaderName", Value: "foobar"},
{Name: "PrioritySampling", Value: "true"},
{Name: "SamplingPriorityHeaderName", Value: "foobar"},
{Name: "TraceIDHeaderName", Value: "foobar"}}},
{Name: "Instana", Children: []*parser.Node{
{Name: "LocalAgentHost", Value: "foobar"},
{Name: "LocalAgentPort", Value: "42"},
{Name: "LogLevel", Value: "foobar"}}},
{Name: "Jaeger", Children: []*parser.Node{
{Name: "Gen128Bit", Value: "true"},
{Name: "LocalAgentHostPort", Value: "foobar"},
{Name: "Propagation", Value: "foobar"},
{Name: "SamplingParam", Value: "42"},
{Name: "SamplingServerURL", Value: "foobar"},
{Name: "SamplingType", Value: "foobar"},
{Name: "TraceContextHeaderName", Value: "foobar"}}},
{Name: "ServiceName", Value: "foobar"},
{Name: "SpanNameLimit", Value: "42"},
{Name: "Zipkin", Children: []*parser.Node{
{Name: "Debug", Value: "true"},
{Name: "HTTPEndpoint", Value: "foobar"},
{Name: "ID128Bit", Value: "true"},
{Name: "SameSpan", Value: "true"},
{Name: "SampleRate", Value: "42"}}}}}},
}
assert.Equal(t, expected, node)
}

View file

@ -0,0 +1,76 @@
package file
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecode_TOML(t *testing.T) {
f, err := ioutil.TempFile("", "traefik-config-*.toml")
require.NoError(t, err)
defer func() {
_ = os.Remove(f.Name())
}()
_, err = f.Write([]byte(`
foo = "bar"
fii = "bir"
[yi]
`))
require.NoError(t, err)
element := &Yo{
Fuu: "test",
}
err = Decode(f.Name(), element)
require.NoError(t, err)
expected := &Yo{
Foo: "bar",
Fii: "bir",
Fuu: "test",
Yi: &Yi{
Foo: "foo",
Fii: "fii",
},
}
assert.Equal(t, expected, element)
}
func TestDecode_YAML(t *testing.T) {
f, err := ioutil.TempFile("", "traefik-config-*.yaml")
require.NoError(t, err)
defer func() {
_ = os.Remove(f.Name())
}()
_, err = f.Write([]byte(`
foo: bar
fii: bir
yi: {}
`))
require.NoError(t, err)
element := &Yo{
Fuu: "test",
}
err = Decode(f.Name(), element)
require.NoError(t, err)
expected := &Yo{
Foo: "bar",
Fii: "bir",
Fuu: "test",
Yi: &Yi{
Foo: "foo",
Fii: "fii",
},
}
assert.Equal(t, expected, element)
}

View file

@ -0,0 +1,539 @@
[Global]
Debug = true
CheckNewVersion = true
SendAnonymousUsage = true
[ServersTransport]
InsecureSkipVerify = true
RootCAs = ["foobar", "foobar"]
MaxIdleConnsPerHost = 42
[ServersTransport.ForwardingTimeouts]
DialTimeout = 42
ResponseHeaderTimeout = 42
[EntryPoints]
[EntryPoints.EntryPoint0]
Address = "foobar"
[EntryPoints.EntryPoint0.Transport]
[EntryPoints.EntryPoint0.Transport.LifeCycle]
RequestAcceptGraceTimeout = 42
GraceTimeOut = 42
[EntryPoints.EntryPoint0.Transport.RespondingTimeouts]
ReadTimeout = 42
WriteTimeout = 42
IdleTimeout = 42
[EntryPoints.EntryPoint0.ProxyProtocol]
Insecure = true
TrustedIPs = ["foobar", "foobar"]
[EntryPoints.EntryPoint0.ForwardedHeaders]
Insecure = true
TrustedIPs = ["foobar", "foobar"]
[Providers]
ProvidersThrottleDuration = 42
[Providers.Docker]
Watch = true
Endpoint = "foobar"
DefaultRule = "foobar"
ExposedByDefault = true
UseBindPortIP = true
SwarmMode = true
Network = "foobar"
SwarmModeRefreshSeconds = 42
[[Providers.Docker.Constraints]]
Key = "foobar"
MustMatch = true
Value = "foobar"
[[Providers.Docker.Constraints]]
Key = "foobar"
MustMatch = true
Value = "foobar"
[Providers.Docker.TLS]
CA = "foobar"
CAOptional = true
Cert = "foobar"
Key = "foobar"
InsecureSkipVerify = true
[Providers.File]
Directory = "foobar"
Watch = true
Filename = "foobar"
DebugLogGeneratedTemplate = true
TraefikFile = "foobar"
[Providers.Marathon]
Trace = true
Watch = true
Endpoint = "foobar"
DefaultRule = "foobar"
ExposedByDefault = true
DCOSToken = "foobar"
FilterMarathonConstraints = true
DialerTimeout = 42
ResponseHeaderTimeout = 42
TLSHandshakeTimeout = 42
KeepAlive = 42
ForceTaskHostname = true
RespectReadinessChecks = true
[[Providers.Marathon.Constraints]]
Key = "foobar"
MustMatch = true
Value = "foobar"
[[Providers.Marathon.Constraints]]
Key = "foobar"
MustMatch = true
Value = "foobar"
[Providers.Marathon.TLS]
CA = "foobar"
CAOptional = true
Cert = "foobar"
Key = "foobar"
InsecureSkipVerify = true
[Providers.Marathon.Basic]
HTTPBasicAuthUser = "foobar"
HTTPBasicPassword = "foobar"
[Providers.Kubernetes]
Endpoint = "foobar"
Token = "foobar"
CertAuthFilePath = "foobar"
DisablePassHostHeaders = true
Namespaces = ["foobar", "foobar"]
LabelSelector = "foobar"
IngressClass = "foobar"
[Providers.Kubernetes.IngressEndpoint]
IP = "foobar"
Hostname = "foobar"
PublishedService = "foobar"
[Providers.KubernetesCRD]
Endpoint = "foobar"
Token = "foobar"
CertAuthFilePath = "foobar"
DisablePassHostHeaders = true
Namespaces = ["foobar", "foobar"]
LabelSelector = "foobar"
IngressClass = "foobar"
[Providers.Rest]
EntryPoint = "foobar"
[Providers.Rancher]
Watch = true
DefaultRule = "foobar"
ExposedByDefault = true
EnableServiceHealthFilter = true
RefreshSeconds = 42
IntervalPoll = true
Prefix = "foobar"
[[Providers.Rancher.Constraints]]
Key = "foobar"
MustMatch = true
Value = "foobar"
[[Providers.Rancher.Constraints]]
Key = "foobar"
MustMatch = true
Value = "foobar"
[API]
EntryPoint = "foobar"
Dashboard = true
Middlewares = ["foobar", "foobar"]
[API.Statistics]
RecentErrors = 42
[Metrics]
[Metrics.Prometheus]
Buckets = [42.0, 42.0]
EntryPoint = "foobar"
Middlewares = ["foobar", "foobar"]
[Metrics.Datadog]
Address = "foobar"
PushInterval = "10s"
[Metrics.StatsD]
Address = "foobar"
PushInterval = "10s"
[Metrics.InfluxDB]
Address = "foobar"
Protocol = "foobar"
PushInterval = "10s"
Database = "foobar"
RetentionPolicy = "foobar"
Username = "foobar"
Password = "foobar"
[Ping]
EntryPoint = "foobar"
Middlewares = ["foobar", "foobar"]
[Log]
Level = "foobar"
FilePath = "foobar"
Format = "foobar"
[AccessLog]
FilePath = "foobar"
Format = "foobar"
BufferingSize = 42
[AccessLog.Filters]
StatusCodes = ["foobar", "foobar"]
RetryAttempts = true
MinDuration = 42
[AccessLog.Fields]
DefaultMode = "foobar"
[AccessLog.Fields.Names]
name0 = "foobar"
name1 = "foobar"
[AccessLog.Fields.Headers]
DefaultMode = "foobar"
[AccessLog.Fields.Headers.Names]
name0 = "foobar"
name1 = "foobar"
[Tracing]
Backend = "foobar"
ServiceName = "foobar"
SpanNameLimit = 42
[Tracing.Jaeger]
SamplingServerURL = "foobar"
SamplingType = "foobar"
SamplingParam = 42.0
LocalAgentHostPort = "foobar"
Gen128Bit = true
Propagation = "foobar"
TraceContextHeaderName = "foobar"
[Tracing.Zipkin]
HTTPEndpoint = "foobar"
SameSpan = true
ID128Bit = true
Debug = true
SampleRate = 42.0
[Tracing.DataDog]
LocalAgentHostPort = "foobar"
GlobalTag = "foobar"
Debug = true
PrioritySampling = true
TraceIDHeaderName = "foobar"
ParentIDHeaderName = "foobar"
SamplingPriorityHeaderName = "foobar"
BagagePrefixHeaderName = "foobar"
[Tracing.Instana]
LocalAgentHost = "foobar"
LocalAgentPort = 42
LogLevel = "foobar"
[HostResolver]
CnameFlattening = true
ResolvConfig = "foobar"
ResolvDepth = 42
[ACME]
Email = "foobar"
ACMELogging = true
CAServer = "foobar"
Storage = "foobar"
EntryPoint = "foobar"
KeyType = "foobar"
OnHostRule = true
[ACME.DNSChallenge]
Provider = "foobar"
DelayBeforeCheck = 42
Resolvers = ["foobar", "foobar"]
DisablePropagationCheck = true
[ACME.HTTPChallenge]
EntryPoint = "foobar"
[ACME.TLSChallenge]
[[ACME.Domains]]
Main = "foobar"
SANs = ["foobar", "foobar"]
[[ACME.Domains]]
Main = "foobar"
SANs = ["foobar", "foobar"]
#### Dynamic configuration
[HTTP]
[HTTP.Routers]
[HTTP.Routers.Router0]
EntryPoints = ["foobar", "foobar"]
Middlewares = ["foobar", "foobar"]
Service = "foobar"
Rule = "foobar"
priority = 42
[HTTP.Routers.Router0.tls]
[HTTP.Middlewares]
[HTTP.Middlewares.Middleware0.AddPrefix]
Prefix = "foobar"
[HTTP.Middlewares.Middleware1.StripPrefix]
Prefixes = ["foobar", "foobar"]
[HTTP.Middlewares.Middleware2.StripPrefixRegex]
Regex = ["foobar", "foobar"]
[HTTP.Middlewares.Middleware3.ReplacePath]
Path = "foobar"
[HTTP.Middlewares.Middleware4.ReplacePathRegex]
Regex = "foobar"
Replacement = "foobar"
[HTTP.Middlewares.Middleware5.Chain]
Middlewares = ["foobar", "foobar"]
[HTTP.Middlewares.Middleware6.IPWhiteList]
SourceRange = ["foobar", "foobar"]
[HTTP.Middlewares.Middleware7.IPWhiteList.IPStrategy]
Depth = 42
ExcludedIPs = ["foobar", "foobar"]
[HTTP.Middlewares.Middleware8.Headers]
AccessControlAllowCredentials = true
AccessControlAllowHeaders = ["foobar", "foobar"]
AccessControlAllowMethods = ["foobar", "foobar"]
AccessControlAllowOrigin = "foobar"
AccessControlExposeHeaders = ["foobar", "foobar"]
AccessControlMaxAge = 42
AddVaryHeader = true
AllowedHosts = ["foobar", "foobar"]
HostsProxyHeaders = ["foobar", "foobar"]
SSLRedirect = true
SSLTemporaryRedirect = true
SSLHost = "foobar"
SSLForceHost = true
STSSeconds = 42
STSIncludeSubdomains = true
STSPreload = true
ForceSTSHeader = true
FrameDeny = true
CustomFrameOptionsValue = "foobar"
ContentTypeNosniff = true
BrowserXSSFilter = true
CustomBrowserXSSValue = "foobar"
ContentSecurityPolicy = "foobar"
PublicKey = "foobar"
ReferrerPolicy = "foobar"
IsDevelopment = true
[HTTP.Middlewares.Middleware8.Headers.CustomRequestHeaders]
name0 = "foobar"
name1 = "foobar"
[HTTP.Middlewares.Middleware8.Headers.CustomResponseHeaders]
name0 = "foobar"
name1 = "foobar"
[HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders]
name0 = "foobar"
name1 = "foobar"
[HTTP.Middlewares.Middleware9.Errors]
Status = ["foobar", "foobar"]
Service = "foobar"
Query = "foobar"
[HTTP.Middlewares.Middleware10.RateLimit]
ExtractorFunc = "foobar"
[HTTP.Middlewares.Middleware10.RateLimit.RateSet]
[HTTP.Middlewares.Middleware10.RateLimit.RateSet.Rate0]
Period = 42
Average = 42
Burst = 42
[HTTP.Middlewares.Middleware10.RateLimit.RateSet.Rate1]
Period = 42
Average = 42
Burst = 42
[HTTP.Middlewares.Middleware11.RedirectRegex]
Regex = "foobar"
Replacement = "foobar"
Permanent = true
[HTTP.Middlewares.Middleware12.RedirectScheme]
Scheme = "foobar"
Port = "foobar"
Permanent = true
[HTTP.Middlewares.Middleware13.BasicAuth]
Users = ["foobar", "foobar"]
UsersFile = "foobar"
Realm = "foobar"
RemoveHeader = true
HeaderField = "foobar"
[HTTP.Middlewares.Middleware14.DigestAuth]
Users = ["foobar", "foobar"]
UsersFile = "foobar"
RemoveHeader = true
Realm = "foobar"
HeaderField = "foobar"
[HTTP.Middlewares.Middleware15.ForwardAuth]
Address = "foobar"
TrustForwardHeader = true
AuthResponseHeaders = ["foobar", "foobar"]
[HTTP.Middlewares.Middleware15.ForwardAuth.TLS]
CA = "foobar"
CAOptional = true
Cert = "foobar"
Key = "foobar"
InsecureSkipVerify = true
[HTTP.Middlewares.Middleware16.MaxConn]
Amount = 42
ExtractorFunc = "foobar"
[HTTP.Middlewares.Middleware17.Buffering]
MaxRequestBodyBytes = 42
MemRequestBodyBytes = 42
MaxResponseBodyBytes = 42
MemResponseBodyBytes = 42
RetryExpression = "foobar"
[HTTP.Middlewares.Middleware18.CircuitBreaker]
Expression = "foobar"
[HTTP.Middlewares.Middleware19.Compress]
[HTTP.Middlewares.Middleware20.PassTLSClientCert]
PEM = true
[HTTP.Middlewares.Middleware20.PassTLSClientCert.Info]
NotAfter = true
NotBefore = true
Sans = true
[HTTP.Middlewares.Middleware20.PassTLSClientCert.Info.Subject]
Country = true
Province = true
Locality = true
Organization = true
CommonName = true
SerialNumber = true
DomainComponent = true
[HTTP.Middlewares.Middleware20.PassTLSClientCert.Info.Issuer]
Country = true
Province = true
Locality = true
Organization = true
CommonName = true
SerialNumber = true
DomainComponent = true
[HTTP.Middlewares.Middleware21.Retry]
Attempts = 42
[HTTP.Services]
[HTTP.Services.Service0]
[HTTP.Services.Service0.LoadBalancer]
Method = "foobar"
PassHostHeader = true
[[HTTP.Services.Service0.LoadBalancer.Servers]]
URL = "foobar"
[HTTP.Services.Service0.LoadBalancer.Stickiness]
CookieName = "foobar"
[[HTTP.Services.Service0.LoadBalancer.Servers]]
URL = "foobar"
[HTTP.Services.Service0.LoadBalancer.HealthCheck]
Scheme = "foobar"
Path = "foobar"
Port = 42
Interval = "foobar"
Timeout = "foobar"
Hostname = "foobar"
[HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers]
name0 = "foobar"
name1 = "foobar"
[HTTP.Services.Service0.LoadBalancer.ResponseForwarding]
FlushInterval = "foobar"
[TCP]
[TCP.Routers]
[TCP.Routers.TCPRouter0]
EntryPoints = ["foobar", "foobar"]
Service = "foobar"
Rule = "foobar"
[TCP.Routers.TCPRouter0.tls]
passthrough = true
[TCP.Services]
[TCP.Services.TCPService0]
[TCP.Services.TCPService0.LoadBalancer]
Method = "foobar"
[[TCP.Services.TCPService0.LoadBalancer.Servers]]
Address = "foobar"
[[TCP.Services.TCPService0.LoadBalancer.Servers]]
Address = "foobar"
[[TLS]]
Stores = ["foobar", "foobar"]
[TLS.Certificate]
CertFile = "foobar"
KeyFile = "foobar"
[[TLS]]
Stores = ["foobar", "foobar"]
[TLS.Certificate]
CertFile = "foobar"
KeyFile = "foobar"
[TLSOptions]
[TLSOptions.TLS0]
MinVersion = "foobar"
CipherSuites = ["foobar", "foobar"]
SniStrict = true
[TLSOptions.TLS0.ClientCA]
Files = ["foobar", "foobar"]
Optional = true
[TLSOptions.TLS1]
MinVersion = "foobar"
CipherSuites = ["foobar", "foobar"]
SniStrict = true
[TLSOptions.TLS1.ClientCA]
Files = ["foobar", "foobar"]
Optional = true
[TLSStores]
[TLSStores.Store0]
[TLSStores.Store0.DefaultCertificate]
CertFile = "foobar"
KeyFile = "foobar"
[TLSStores.Store1]
[TLSStores.Store1.DefaultCertificate]
CertFile = "foobar"
KeyFile = "foobar"

View file

@ -0,0 +1,257 @@
Global:
Debug: true
CheckNewVersion: true
SendAnonymousUsage: true
ServersTransport:
InsecureSkipVerify: true
RootCAs:
- foobar
- foobar
MaxIdleConnsPerHost: 42
ForwardingTimeouts:
DialTimeout: 42
ResponseHeaderTimeout: 42
EntryPoints:
EntryPoint0:
Address: foobar
Transport:
LifeCycle:
RequestAcceptGraceTimeout: 42
GraceTimeOut: 42
RespondingTimeouts:
ReadTimeout: 42
WriteTimeout: 42
IdleTimeout: 42
ProxyProtocol:
Insecure: true
TrustedIPs:
- foobar
- foobar
ForwardedHeaders:
Insecure: true
TrustedIPs:
- foobar
- foobar
Providers:
ProvidersThrottleDuration: 42
Docker:
Watch: true
Endpoint: foobar
DefaultRule: foobar
ExposedByDefault: true
UseBindPortIP: true
SwarmMode: true
Network: foobar
SwarmModeRefreshSeconds: 42
Constraints:
- Key: foobar
MustMatch: true
Value: foobar
- Key: foobar
MustMatch: true
Value: foobar
TLS:
CA: foobar
CAOptional: true
Cert: foobar
Key: foobar
InsecureSkipVerify: true
File:
Directory: foobar
Watch: true
Filename: foobar
DebugLogGeneratedTemplate: true
TraefikFile: foobar
Marathon:
Trace: true
Watch: true
Endpoint: foobar
DefaultRule: foobar
ExposedByDefault: true
DCOSToken: foobar
FilterMarathonConstraints: true
DialerTimeout: 42
ResponseHeaderTimeout: 42
TLSHandshakeTimeout: 42
KeepAlive: 42
ForceTaskHostname: true
RespectReadinessChecks: true
Constraints:
- Key: foobar
MustMatch: true
Value: foobar
- Key: foobar
MustMatch: true
Value: foobar
TLS:
CA: foobar
CAOptional: true
Cert: foobar
Key: foobar
InsecureSkipVerify: true
Basic:
HTTPBasicAuthUser: foobar
HTTPBasicPassword: foobar
Kubernetes:
Endpoint: foobar
Token: foobar
CertAuthFilePath: foobar
DisablePassHostHeaders: true
Namespaces:
- foobar
- foobar
LabelSelector: foobar
IngressClass: foobar
IngressEndpoint:
IP: foobar
Hostname: foobar
PublishedService: foobar
KubernetesCRD:
Endpoint: foobar
Token: foobar
CertAuthFilePath: foobar
DisablePassHostHeaders: true
Namespaces:
- foobar
- foobar
LabelSelector: foobar
IngressClass: foobar
Rest:
EntryPoint: foobar
Rancher:
Watch: true
DefaultRule: foobar
ExposedByDefault: true
EnableServiceHealthFilter: true
RefreshSeconds: 42
IntervalPoll: true
Prefix: foobar
Constraints:
- Key: foobar
MustMatch: true
Value: foobar
- Key: foobar
MustMatch: true
Value: foobar
API:
EntryPoint: foobar
Dashboard: true
Middlewares:
- foobar
- foobar
Statistics:
RecentErrors: 42
Metrics:
Prometheus:
Buckets:
- 42
- 42
EntryPoint: foobar
Middlewares:
- foobar
- foobar
Datadog:
Address: foobar
PushInterval: 10s
StatsD:
Address: foobar
PushInterval: 10s
InfluxDB:
Address: foobar
Protocol: foobar
PushInterval: 10s
Database: foobar
RetentionPolicy: foobar
Username: foobar
Password: foobar
Ping:
EntryPoint: foobar
Middlewares:
- foobar
- foobar
Log:
Level: foobar
FilePath: foobar
Format: foobar
AccessLog:
FilePath: foobar
Format: foobar
BufferingSize: 42
Filters:
StatusCodes:
- foobar
- foobar
RetryAttempts: true
MinDuration: 42
Fields:
DefaultMode: foobar
Names:
name0: foobar
name1: foobar
Headers:
DefaultMode: foobar
Names:
name0: foobar
name1: foobar
Tracing:
Backend: foobar
ServiceName: foobar
SpanNameLimit: 42
Jaeger:
SamplingServerURL: foobar
SamplingType: foobar
SamplingParam: 42
LocalAgentHostPort: foobar
Gen128Bit: true
Propagation: foobar
TraceContextHeaderName: foobar
Zipkin:
HTTPEndpoint: foobar
SameSpan: true
ID128Bit: true
Debug: true
SampleRate: 42
DataDog:
LocalAgentHostPort: foobar
GlobalTag: foobar
Debug: true
PrioritySampling: true
TraceIDHeaderName: foobar
ParentIDHeaderName: foobar
SamplingPriorityHeaderName: foobar
BagagePrefixHeaderName: foobar
Instana:
LocalAgentHost: foobar
LocalAgentPort: 42
LogLevel: foobar
HostResolver:
CnameFlattening: true
ResolvConfig: foobar
ResolvDepth: 42
ACME:
Email: foobar
ACMELogging: true
CAServer: foobar
Storage: foobar
EntryPoint: foobar
KeyType: foobar
OnHostRule: true
DNSChallenge:
Provider: foobar
DelayBeforeCheck: 42
Resolvers:
- foobar
- foobar
DisablePropagationCheck: true
HTTPChallenge:
EntryPoint: foobar
TLSChallenge: {}
Domains:
- Main: foobar
SANs:
- foobar
- foobar
- Main: foobar
SANs:
- foobar
- foobar

View file

@ -0,0 +1,34 @@
package file
type bar string
type Yo struct {
Foo string
Fii string
Fuu string
Yi *Yi `label:"allowEmpty"`
}
func (y *Yo) SetDefaults() {
y.Foo = "foo"
y.Fii = "fii"
}
type Yi struct {
Foo string
Fii string
Fuu string
}
func (y *Yi) SetDefaults() {
y.Foo = "foo"
y.Fii = "fii"
}
type Yu struct {
Yi
}
type Ye struct {
*Yi
}

128
pkg/config/file/raw_node.go Normal file
View file

@ -0,0 +1,128 @@
package file
import (
"reflect"
"sort"
"strconv"
"strings"
"github.com/containous/traefik/pkg/config/parser"
)
func decodeRawToNode(data map[string]interface{}, filters ...string) (*parser.Node, error) {
root := &parser.Node{
Name: "traefik",
}
vData := reflect.ValueOf(data)
decodeRaw(root, vData, filters...)
return root, nil
}
func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) {
sortedKeys := sortKeys(vData, filters)
for _, key := range sortedKeys {
value := reflect.ValueOf(vData.MapIndex(key).Interface())
child := &parser.Node{Name: key.String()}
switch value.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fallthrough
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fallthrough
case reflect.Float32, reflect.Float64:
fallthrough
case reflect.Bool:
fallthrough
case reflect.String:
child.Value = getSimpleValue(value)
case reflect.Slice:
var values []string
for i := 0; i < value.Len(); i++ {
item := value.Index(i)
switch item.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fallthrough
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fallthrough
case reflect.Bool:
fallthrough
case reflect.String:
fallthrough
case reflect.Map:
fallthrough
case reflect.Interface:
sValue := reflect.ValueOf(item.Interface())
if sValue.Kind() == reflect.Map {
ch := &parser.Node{
Name: "[" + strconv.Itoa(i) + "]",
}
child.Children = append(child.Children, ch)
decodeRaw(ch, sValue)
} else {
values = append(values, getSimpleValue(sValue))
}
default:
panic("Unsupported slice type: " + item.Kind().String())
}
}
child.Value = strings.Join(values, ",")
case reflect.Map:
decodeRaw(child, value)
default:
panic("Unsupported type: " + value.Kind().String())
}
node.Children = append(node.Children, child)
}
}
func getSimpleValue(item reflect.Value) string {
switch item.Kind() {
case reflect.String:
return item.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(item.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(item.Uint(), 10)
case reflect.Float32, reflect.Float64:
return strings.TrimSuffix(strconv.FormatFloat(item.Float(), 'f', 6, 64), ".000000")
case reflect.Bool:
return strconv.FormatBool(item.Bool())
default:
panic("Unsupported Simple value type: " + item.Kind().String())
}
}
func sortKeys(vData reflect.Value, filters []string) []reflect.Value {
var sortedKeys []reflect.Value
for _, v := range vData.MapKeys() {
rValue := reflect.ValueOf(v.Interface())
key := rValue.String()
if len(filters) == 0 {
sortedKeys = append(sortedKeys, rValue)
continue
}
for _, filter := range filters {
if strings.EqualFold(key, filter) {
sortedKeys = append(sortedKeys, rValue)
continue
}
}
}
sort.Slice(sortedKeys, func(i, j int) bool {
return sortedKeys[i].String() < sortedKeys[j].String()
})
return sortedKeys
}

View file

@ -0,0 +1,540 @@
package file
import (
"testing"
"github.com/containous/traefik/pkg/config/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_decodeRawToNode(t *testing.T) {
testCases := []struct {
desc string
data map[string]interface{}
expected *parser.Node
}{
{
desc: "empty",
data: map[string]interface{}{},
expected: &parser.Node{
Name: "traefik",
},
},
{
desc: "string",
data: map[string]interface{}{
"foo": "bar",
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "bar"},
},
},
},
{
desc: "string named type",
data: map[string]interface{}{
"foo": bar("bar"),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "bar"},
},
},
},
{
desc: "bool",
data: map[string]interface{}{
"foo": true,
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "true"},
},
},
},
{
desc: "int",
data: map[string]interface{}{
"foo": 1,
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "int8",
data: map[string]interface{}{
"foo": int8(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "int16",
data: map[string]interface{}{
"foo": int16(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "int32",
data: map[string]interface{}{
"foo": int32(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "int64",
data: map[string]interface{}{
"foo": int64(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "uint",
data: map[string]interface{}{
"foo": uint(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "uint8",
data: map[string]interface{}{
"foo": uint8(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "uint16",
data: map[string]interface{}{
"foo": uint16(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "uint32",
data: map[string]interface{}{
"foo": uint32(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "uint64",
data: map[string]interface{}{
"foo": uint64(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "float32",
data: map[string]interface{}{
"foo": float32(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "float64",
data: map[string]interface{}{
"foo": float64(1),
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1"},
},
},
},
{
desc: "string slice",
data: map[string]interface{}{
"foo": []string{"A", "B"},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "A,B"},
},
},
},
{
desc: "int slice",
data: map[string]interface{}{
"foo": []int{1, 2},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1,2"},
},
},
},
{
desc: "int8 slice",
data: map[string]interface{}{
"foo": []int8{1, 2},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1,2"},
},
},
},
{
desc: "int16 slice",
data: map[string]interface{}{
"foo": []int16{1, 2},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1,2"},
},
},
},
{
desc: "int32 slice",
data: map[string]interface{}{
"foo": []int32{1, 2},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1,2"},
},
},
},
{
desc: "int64 slice",
data: map[string]interface{}{
"foo": []int64{1, 2},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1,2"},
},
},
},
{
desc: "bool slice",
data: map[string]interface{}{
"foo": []bool{true, false},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "true,false"},
},
},
},
{
desc: "interface (string) slice",
data: map[string]interface{}{
"foo": []interface{}{"A", "B"},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "A,B"},
},
},
},
{
desc: "interface (int) slice",
data: map[string]interface{}{
"foo": []interface{}{1, 2},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Value: "1,2"},
},
},
},
{
desc: "2 strings",
data: map[string]interface{}{
"foo": "bar",
"fii": "bir",
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Value: "bir"},
{Name: "foo", Value: "bar"},
},
},
},
{
desc: "string, level 2",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": "bur",
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "bur"}}},
},
},
},
{
desc: "int, level 2",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": 1,
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
},
},
},
{
desc: "uint, level 2",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": uint(1),
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
},
},
},
{
desc: "bool, level 2",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": true,
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "true"}}},
},
},
},
{
desc: "string, level 3",
data: map[string]interface{}{
"foo": map[interface{}]interface{}{
"fii": map[interface{}]interface{}{
"fuu": "bur",
},
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "bur"}}},
}},
},
},
},
{
desc: "int, level 3",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": 1,
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
},
},
},
{
desc: "uint, level 3",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": uint(1),
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
},
},
},
{
desc: "bool, level 3",
data: map[string]interface{}{
"fii": map[interface{}]interface{}{
"fuu": true,
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "true"}}},
},
},
},
{
desc: "struct",
data: map[string]interface{}{
"foo": map[interface{}]interface{}{
"field1": "C",
"field2": "C",
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Children: []*parser.Node{
{Name: "field1", Value: "C"},
{Name: "field2", Value: "C"},
}},
},
},
},
{
desc: "slice struct 1",
data: map[string]interface{}{
"foo": []map[string]interface{}{
{"field1": "A", "field2": "A"},
{"field1": "B", "field2": "B"},
{"field2": "C", "field1": "C"},
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "field1", Value: "A"},
{Name: "field2", Value: "A"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "field1", Value: "B"},
{Name: "field2", Value: "B"},
}},
{Name: "[2]", Children: []*parser.Node{
{Name: "field1", Value: "C"},
{Name: "field2", Value: "C"},
}},
}},
},
},
},
{
desc: "slice struct 2",
data: map[string]interface{}{
"foo": []interface{}{
map[interface{}]interface{}{
"field2": "A",
"field1": "A",
},
map[interface{}]interface{}{
"field1": "B",
"field2": "B",
},
map[interface{}]interface{}{
"field1": "C",
"field2": "C",
},
},
},
expected: &parser.Node{
Name: "traefik",
Children: []*parser.Node{
{Name: "foo", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "field1", Value: "A"},
{Name: "field2", Value: "A"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "field1", Value: "B"},
{Name: "field2", Value: "B"},
}},
{Name: "[2]", Children: []*parser.Node{
{Name: "field1", Value: "C"},
{Name: "field2", Value: "C"},
}},
}},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
node, err := decodeRawToNode(test.data)
require.NoError(t, err)
assert.Equal(t, test.expected, node)
})
}
}

44
pkg/config/flag/flag.go Normal file
View file

@ -0,0 +1,44 @@
// Package flag implements encoding and decoding between flag arguments and a typed Configuration.
package flag
import (
"github.com/containous/traefik/pkg/config/parser"
)
// Decode decodes the given flag arguments into the given element.
// The operation goes through four stages roughly summarized as:
// flag arguments -> parsed map of flags
// map -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element
func Decode(args []string, element interface{}) error {
ref, err := Parse(args, element)
if err != nil {
return err
}
return parser.Decode(ref, element)
}
// Encode encodes the configuration in element into the flags represented in the returned Flats.
// The operation goes through three stages roughly summarized as:
// typed configuration in element -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> flags with default values (determined by type/kind)
func Encode(element interface{}) ([]parser.Flat, error) {
if element == nil {
return nil, nil
}
node, err := parser.EncodeToNode(element, false)
if err != nil {
return nil, err
}
err = parser.AddMetadata(element, node)
if err != nil {
return nil, err
}
return parser.EncodeToFlat(element, node, parser.FlatOpts{Separator: ".", SkipRoot: true})
}

View file

@ -0,0 +1,926 @@
package flag
import (
"testing"
"time"
"github.com/containous/traefik/pkg/config/generator"
"github.com/containous/traefik/pkg/config/parser"
"github.com/containous/traefik/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecode(t *testing.T) {
testCases := []struct {
desc string
args []string
element interface{}
expected interface{}
}{
{
desc: "no args",
args: nil,
expected: nil,
},
{
desc: "types.Duration value",
args: []string{"--foo=1"},
element: &struct {
Foo types.Duration
}{},
expected: &struct {
Foo types.Duration
}{
Foo: types.Duration(1 * time.Second),
},
},
{
desc: "time.Duration value",
args: []string{"--foo=1"},
element: &struct {
Foo time.Duration
}{},
expected: &struct {
Foo time.Duration
}{
Foo: 1 * time.Nanosecond,
},
},
{
desc: "bool value",
args: []string{"--foo"},
element: &struct {
Foo bool
}{},
expected: &struct {
Foo bool
}{
Foo: true,
},
},
{
desc: "equal",
args: []string{"--foo=bar"},
element: &struct {
Foo string
}{},
expected: &struct {
Foo string
}{
Foo: "bar",
},
},
{
desc: "space separated",
args: []string{"--foo", "bar"},
element: &struct {
Foo string
}{},
expected: &struct {
Foo string
}{
Foo: "bar",
},
},
{
desc: "space separated with end of parameter",
args: []string{"--foo=bir", "--", "--bar"},
element: &struct {
Foo string
}{},
expected: &struct {
Foo string
}{
Foo: "bir",
},
},
{
desc: "multiple bool flags without value",
args: []string{"--foo", "--bar"},
element: &struct {
Foo bool
Bar bool
}{},
expected: &struct {
Foo bool
Bar bool
}{
Foo: true,
Bar: true,
},
},
{
desc: "slice with several flags",
args: []string{"--foo=bar", "--foo=baz"},
element: &struct {
Foo []string
}{},
expected: &struct {
Foo []string
}{
Foo: []string{"bar", "baz"},
},
},
{
desc: "map string",
args: []string{"--foo.name=bar"},
element: &struct {
Foo map[string]string
}{},
expected: &struct {
Foo map[string]string
}{
Foo: map[string]string{
"name": "bar",
},
},
},
{
desc: "map struct",
args: []string{"--foo.name.value=bar"},
element: &struct {
Foo map[string]struct{ Value string }
}{},
expected: &struct {
Foo map[string]struct{ Value string }
}{
Foo: map[string]struct{ Value string }{
"name": {
Value: "bar",
},
},
},
},
{
desc: "map struct with sub-struct",
args: []string{"--foo.name.bar.value=bar"},
element: &struct {
Foo map[string]struct {
Bar *struct{ Value string }
}
}{},
expected: &struct {
Foo map[string]struct {
Bar *struct{ Value string }
}
}{
Foo: map[string]struct {
Bar *struct{ Value string }
}{
"name": {
Bar: &struct {
Value string
}{
Value: "bar",
},
},
},
},
},
{
desc: "map struct with sub-map",
args: []string{"--foo.name1.bar.name2.value=bar"},
element: &struct {
Foo map[string]struct {
Bar map[string]struct{ Value string }
}
}{},
expected: &struct {
Foo map[string]struct {
Bar map[string]struct{ Value string }
}
}{
Foo: map[string]struct {
Bar map[string]struct{ Value string }
}{
"name1": {
Bar: map[string]struct{ Value string }{
"name2": {
Value: "bar",
},
},
},
},
},
},
{
desc: "slice with several flags 2",
args: []string{"--foo", "bar", "--foo", "baz"},
element: &struct {
Foo []string
}{},
expected: &struct {
Foo []string
}{
Foo: []string{"bar", "baz"},
},
},
{
desc: "slice with several flags 3",
args: []string{"--foo", "bar", "--foo=", "--baz"},
element: &struct {
Foo []string
Baz bool
}{},
expected: &struct {
Foo []string
Baz bool
}{
Foo: []string{"bar", ""},
Baz: true,
},
},
{
desc: "slice with several flags 4",
args: []string{"--foo", "bar", "--foo", "--baz"},
element: &struct {
Foo []string
Baz bool
}{},
expected: &struct {
Foo []string
Baz bool
}{
Foo: []string{"bar", "--baz"},
},
},
{
desc: "slice of struct",
args: []string{
"--foo[0].Field1", "bar", "--foo[0].Field2", "6",
"--foo[1].Field1", "bur", "--foo[1].Field2", "2",
},
element: &struct {
Foo []struct {
Field1 string
Field2 int
}
}{},
expected: &struct {
Foo []struct {
Field1 string
Field2 int
}
}{
Foo: []struct {
Field1 string
Field2 int
}{
{
Field1: "bar",
Field2: 6,
},
{
Field1: "bur",
Field2: 2,
},
},
},
},
{
desc: "slice of pointer of struct",
args: []string{
"--foo[0].Field1", "bar", "--foo[0].Field2", "6",
"--foo[1].Field1", "bur", "--foo[1].Field2", "2",
},
element: &struct {
Foo []*struct {
Field1 string
Field2 int
}
}{},
expected: &struct {
Foo []*struct {
Field1 string
Field2 int
}
}{
Foo: []*struct {
Field1 string
Field2 int
}{
{
Field1: "bar",
Field2: 6,
},
{
Field1: "bur",
Field2: 2,
},
},
},
},
{
desc: "multiple string flag",
element: &struct {
Foo string
}{},
args: []string{"--foo=bar", "--foo=baz"},
expected: &struct {
Foo string
}{
Foo: "baz",
},
},
{
desc: "multiple string flag 2",
element: &struct {
Foo string
}{},
args: []string{"--foo", "bar", "--foo", "baz"},
expected: &struct {
Foo string
}{
Foo: "baz",
},
},
{
desc: "string without value",
element: &struct {
Foo string
Bar bool
}{},
args: []string{"--foo", "--bar"},
expected: &struct {
Foo string
Bar bool
}{
Foo: "--bar",
},
},
{
desc: "struct pointer value",
args: []string{"--foo"},
element: &struct {
Foo *struct{ Field string } `label:"allowEmpty"`
}{},
expected: &struct {
Foo *struct{ Field string } `label:"allowEmpty"`
}{
Foo: &struct{ Field string }{},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := Decode(test.args, test.element)
require.NoError(t, err)
assert.Equal(t, test.expected, test.element)
})
}
}
func TestEncode(t *testing.T) {
testCases := []struct {
desc string
element interface{}
expected []parser.Flat
}{
{
desc: "string field",
element: &struct {
Field string `description:"field description"`
}{
Field: "test",
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "test",
}},
},
{
desc: "int field",
element: &struct {
Field int `description:"field description"`
}{
Field: 6,
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "6",
}},
},
{
desc: "bool field",
element: &struct {
Field bool `description:"field description"`
}{
Field: true,
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "true",
}},
},
{
desc: "string pointer field",
element: &struct {
Field *string `description:"field description"`
}{
Field: func(v string) *string { return &v }("test"),
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "test",
}},
},
{
desc: "int pointer field",
element: &struct {
Field *int `description:"field description"`
}{
Field: func(v int) *int { return &v }(6),
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "6",
}},
},
{
desc: "bool pointer field",
element: &struct {
Field *bool `description:"field description"`
}{
Field: func(v bool) *bool { return &v }(true),
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "true",
}},
},
{
desc: "slice of string field, no initial value",
element: &struct {
Field []string `description:"field description"`
}{},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "",
}},
},
{
desc: "slice of string field, with initial value",
element: &struct {
Field []string `description:"field description"`
}{
Field: []string{"foo", "bar"},
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "foo, bar",
}},
},
{
desc: "slice of int field, no initial value",
element: &struct {
Field []int `description:"field description"`
}{},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "",
}},
},
{
desc: "slice of int field, with initial value",
element: &struct {
Field []int `description:"field description"`
}{
Field: []int{6, 3},
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "6, 3",
}},
},
{
desc: "map string field",
element: &struct {
Field map[string]string `description:"field description"`
}{
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
expected: []parser.Flat{{
Name: "field.<name>",
Description: "field description",
Default: "",
}},
},
{
desc: "struct pointer field",
element: &struct {
Foo *struct {
Field string `description:"field description"`
} `description:"foo description"`
}{
Foo: &struct {
Field string `description:"field description"`
}{
Field: "test",
},
},
expected: []parser.Flat{
{
Name: "foo.field",
Description: "field description",
Default: "test",
},
},
},
{
desc: "struct pointer field, allow empty",
element: &struct {
Foo *struct {
Field string `description:"field description"`
} `description:"foo description" label:"allowEmpty"`
}{
Foo: &struct {
Field string `description:"field description"`
}{
Field: "test",
},
},
expected: []parser.Flat{
{
Name: "foo",
Description: "foo description",
Default: "false",
},
{
Name: "foo.field",
Description: "field description",
Default: "test",
},
},
},
{
desc: "struct pointer field level 2",
element: &struct {
Foo *struct {
Fii *struct {
Field string `description:"field description"`
} `description:"fii description"`
} `description:"foo description"`
}{
Foo: &struct {
Fii *struct {
Field string `description:"field description"`
} `description:"fii description"`
}{
Fii: &struct {
Field string `description:"field description"`
}{
Field: "test",
},
},
},
expected: []parser.Flat{
{
Name: "foo.fii.field",
Description: "field description",
Default: "test",
},
},
},
{
desc: "struct pointer field level 2, allow empty",
element: &struct {
Foo *struct {
Fii *struct {
Field string `description:"field description"`
} `description:"fii description" label:"allowEmpty"`
} `description:"foo description" label:"allowEmpty"`
}{
Foo: &struct {
Fii *struct {
Field string `description:"field description"`
} `description:"fii description" label:"allowEmpty"`
}{
Fii: &struct {
Field string `description:"field description"`
}{
Field: "test",
},
},
},
expected: []parser.Flat{
{
Name: "foo",
Description: "foo description",
Default: "false",
},
{
Name: "foo.fii",
Description: "fii description",
Default: "false",
},
{
Name: "foo.fii.field",
Description: "field description",
Default: "test",
},
},
},
{
desc: "map string field level 2",
element: &struct {
Foo *struct {
Fii map[string]string `description:"fii description"`
} `description:"foo description"`
}{
Foo: &struct {
Fii map[string]string `description:"fii description"`
}{
Fii: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
expected: []parser.Flat{
{
Name: "foo.fii.<name>",
Description: "fii description",
Default: "",
},
},
},
{
desc: "map string pointer field level 2",
element: &struct {
Foo *struct {
Fii map[string]*string `description:"fii description"`
} `description:"foo description"`
}{
Foo: &struct {
Fii map[string]*string `description:"fii description"`
}{
Fii: map[string]*string{
parser.MapNamePlaceholder: func(v string) *string { return &v }(""),
},
},
},
expected: []parser.Flat{
{
Name: "foo.fii.<name>",
Description: "fii description",
Default: "",
},
},
},
{
desc: "map struct level 1",
element: &struct {
Foo map[string]struct {
Field string `description:"field description"`
Yo int `description:"yo description"`
} `description:"foo description"`
}{},
expected: []parser.Flat{
{
Name: "foo.<name>",
Description: "foo description",
Default: "false",
},
{
Name: "foo.<name>.field",
Description: "field description",
Default: "",
},
{
Name: "foo.<name>.yo",
Description: "yo description",
Default: "0",
},
},
},
{
desc: "map struct pointer level 1",
element: &struct {
Foo map[string]*struct {
Field string `description:"field description"`
Yo string `description:"yo description"`
} `description:"foo description"`
}{},
expected: []parser.Flat{
{
Name: "foo.<name>",
Description: "foo description",
Default: "false",
},
{
Name: "foo.<name>.field",
Description: "field description",
Default: "",
},
{
Name: "foo.<name>.yo",
Description: "yo description",
Default: "",
},
},
},
{
desc: "time duration field",
element: &struct {
Field time.Duration `description:"field description"`
}{
Field: 1 * time.Second,
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "1s",
}},
},
{
desc: "time duration field map",
element: &struct {
Foo map[string]*struct {
Field time.Duration `description:"field description"`
} `description:"foo description"`
}{
Foo: map[string]*struct {
Field time.Duration `description:"field description"`
}{},
},
expected: []parser.Flat{
{
Name: "foo.<name>",
Description: "foo description",
Default: "false",
},
{
Name: "foo.<name>.field",
Description: "field description",
Default: "0s",
},
},
},
{
desc: "time duration field map 2",
element: &struct {
Foo map[string]*struct {
Fii *struct {
Field time.Duration `description:"field description"`
}
} `description:"foo description"`
}{
Foo: map[string]*struct {
Fii *struct {
Field time.Duration `description:"field description"`
}
}{},
},
expected: []parser.Flat{
{
Name: "foo.<name>",
Description: "foo description",
Default: "false",
},
{
Name: "foo.<name>.fii.field",
Description: "field description",
Default: "0s",
},
},
},
{
desc: "time duration field 2",
element: &struct {
Foo *struct {
Field time.Duration `description:"field description"`
}
}{
Foo: &struct {
Field time.Duration `description:"field description"`
}{
Field: 1 * time.Second,
},
},
expected: []parser.Flat{{
Name: "foo.field",
Description: "field description",
Default: "1s",
}},
},
{
desc: "time duration field 3",
element: &struct {
Foo *struct {
Fii *struct {
Field time.Duration `description:"field description"`
}
}
}{
Foo: &struct {
Fii *struct {
Field time.Duration `description:"field description"`
}
}{
Fii: &struct {
Field time.Duration `description:"field description"`
}{
Field: 1 * time.Second,
},
},
},
expected: []parser.Flat{{
Name: "foo.fii.field",
Description: "field description",
Default: "1s",
}},
},
{
desc: "time duration field",
element: &struct {
Field types.Duration `description:"field description"`
}{
Field: types.Duration(180 * time.Second),
},
expected: []parser.Flat{{
Name: "field",
Description: "field description",
Default: "180",
}},
},
{
desc: "slice of struct",
element: &struct {
Foo *struct {
Fii []struct {
Field1 string `description:"field1 description"`
Field2 int `description:"field2 description"`
} `description:"fii description"`
} `description:"foo description"`
}{},
expected: []parser.Flat{
{
Name: "foo.fii",
Description: "fii description",
Default: "",
},
{
Name: "foo.fii[0].field1",
Description: "field1 description",
Default: "",
},
{
Name: "foo.fii[0].field2",
Description: "field2 description",
Default: "0",
},
},
},
// Skipped: because realistically not needed in Traefik for now.
// {
// desc: "map of map field level 2",
// element: &struct {
// Foo *struct {
// Fii map[string]map[string]string `description:"fii description"`
// } `description:"foo description"`
// }{
// Foo: &struct {
// Fii map[string]map[string]string `description:"fii description"`
// }{
// Fii: map[string]map[string]string{
// parser.MapNamePlaceholder: {
// parser.MapNamePlaceholder: "test",
// },
// },
// },
// },
// expected: `XXX`,
// },
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
generator.Generate(test.element)
entries, err := Encode(test.element)
require.NoError(t, err)
assert.Equal(t, test.expected, entries)
})
}
}

View file

@ -0,0 +1,108 @@
package flag
import (
"fmt"
"reflect"
"strings"
)
// Parse parses the command-line flag arguments into a map,
// using the type information in element to discriminate whether a flag is supposed to be a bool,
// and other such ambiguities.
func Parse(args []string, element interface{}) (map[string]string, error) {
f := flagSet{
flagTypes: getFlagTypes(element),
args: args,
values: make(map[string]string),
}
for {
seen, err := f.parseOne()
if seen {
continue
}
if err == nil {
break
}
return nil, err
}
return f.values, nil
}
type flagSet struct {
flagTypes map[string]reflect.Kind
args []string
values map[string]string
}
func (f *flagSet) parseOne() (bool, error) {
if len(f.args) == 0 {
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {
return false, nil
}
numMinuses := 1
if s[1] == '-' {
numMinuses++
if len(s) == 2 { // "--" terminates the flags
f.args = f.args[1:]
return false, nil
}
}
name := s[numMinuses:]
if len(name) == 0 || name[0] == '-' || name[0] == '=' {
return false, fmt.Errorf("bad flag syntax: %s", s)
}
// it's a flag. does it have an argument?
f.args = f.args[1:]
hasValue := false
value := ""
for i := 1; i < len(name); i++ { // equals cannot be first
if name[i] == '=' {
value = name[i+1:]
hasValue = true
name = name[0:i]
break
}
}
if hasValue {
f.setValue(name, value)
return true, nil
}
if f.flagTypes[name] == reflect.Bool || f.flagTypes[name] == reflect.Ptr {
f.setValue(name, "true")
return true, nil
}
if len(f.args) > 0 {
// value is the next arg
hasValue = true
value, f.args = f.args[0], f.args[1:]
}
if !hasValue {
return false, fmt.Errorf("flag needs an argument: -%s", name)
}
f.setValue(name, value)
return true, nil
}
func (f *flagSet) setValue(name string, value string) {
n := strings.ToLower("traefik." + name)
v, ok := f.values[n]
if ok && f.flagTypes[name] == reflect.Slice {
f.values[n] = v + "," + value
return
}
f.values[n] = value
}

View file

@ -0,0 +1,255 @@
package flag
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParse(t *testing.T) {
testCases := []struct {
desc string
args []string
element interface{}
expected map[string]string
}{
{
desc: "no args",
args: nil,
expected: map[string]string{},
},
{
desc: "bool value",
args: []string{"--foo"},
element: &struct {
Foo bool
}{},
expected: map[string]string{
"traefik.foo": "true",
},
},
{
desc: "equal",
args: []string{"--foo=bar"},
element: &struct {
Foo string
}{},
expected: map[string]string{
"traefik.foo": "bar",
},
},
{
desc: "space separated",
args: []string{"--foo", "bar"},
element: &struct {
Foo string
}{},
expected: map[string]string{
"traefik.foo": "bar",
},
},
{
desc: "space separated with end of parameter",
args: []string{"--foo=bir", "--", "--bar"},
element: &struct {
Foo string
}{},
expected: map[string]string{
"traefik.foo": "bir",
},
},
{
desc: "multiple bool flags without value",
args: []string{"--foo", "--bar"},
element: &struct {
Foo bool
Bar bool
}{},
expected: map[string]string{
"traefik.foo": "true",
"traefik.bar": "true",
},
},
{
desc: "slice with several flags",
args: []string{"--foo=bar", "--foo=baz"},
element: &struct {
Foo []string
}{},
expected: map[string]string{
"traefik.foo": "bar,baz",
},
},
{
desc: "map string",
args: []string{"--foo.name=bar"},
element: &struct {
Foo map[string]string
}{},
expected: map[string]string{
"traefik.foo.name": "bar",
},
},
{
desc: "map struct",
args: []string{"--foo.name.value=bar"},
element: &struct {
Foo map[string]struct{ Value string }
}{},
expected: map[string]string{
"traefik.foo.name.value": "bar",
},
},
{
desc: "map struct with sub-struct",
args: []string{"--foo.name.bar.value=bar"},
element: &struct {
Foo map[string]struct {
Bar *struct{ Value string }
}
}{},
expected: map[string]string{
"traefik.foo.name.bar.value": "bar",
},
},
{
desc: "map struct with sub-map",
args: []string{"--foo.name1.bar.name2.value=bar"},
element: &struct {
Foo map[string]struct {
Bar map[string]struct{ Value string }
}
}{},
expected: map[string]string{
"traefik.foo.name1.bar.name2.value": "bar",
},
},
{
desc: "slice with several flags 2",
args: []string{"--foo", "bar", "--foo", "baz"},
element: &struct {
Foo []string
}{},
expected: map[string]string{
"traefik.foo": "bar,baz",
},
},
{
desc: "slice with several flags 3",
args: []string{"--foo", "bar", "--foo=", "--baz"},
element: &struct {
Foo []string
Baz bool
}{},
expected: map[string]string{
"traefik.foo": "bar,",
"traefik.baz": "true",
},
},
{
desc: "slice with several flags 4",
args: []string{"--foo", "bar", "--foo", "--baz"},
element: &struct {
Foo []string
Baz bool
}{},
expected: map[string]string{
"traefik.foo": "bar,--baz",
},
},
{
desc: "multiple string flag",
element: &struct {
Foo string
}{},
args: []string{"--foo=bar", "--foo=baz"},
expected: map[string]string{
"traefik.foo": "baz",
},
},
{
desc: "multiple string flag 2",
element: &struct {
Foo string
}{},
args: []string{"--foo", "bar", "--foo", "baz"},
expected: map[string]string{
"traefik.foo": "baz",
},
},
{
desc: "string without value",
element: &struct {
Foo string
Bar bool
}{},
args: []string{"--foo", "--bar"},
expected: map[string]string{
"traefik.foo": "--bar",
},
},
{
desc: "struct pointer value",
args: []string{"--foo"},
element: &struct {
Foo *struct{ Field string }
}{},
expected: map[string]string{
"traefik.foo": "true",
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
fl, err := Parse(test.args, test.element)
require.NoError(t, err)
assert.Equal(t, test.expected, fl)
})
}
}
func TestParse_Errors(t *testing.T) {
testCases := []struct {
desc string
args []string
element interface{}
}{
{
desc: "triple hyphen",
args: []string{"---foo"},
element: &struct {
Foo bool
}{},
},
{
desc: "equal",
args: []string{"--=foo"},
element: &struct {
Foo bool
}{},
},
{
desc: "string without value",
element: &struct {
Foo string
Bar bool
}{},
args: []string{"--foo"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
_, err := Parse(test.args, test.element)
require.Error(t, err)
})
}
}

View file

@ -0,0 +1,60 @@
package flag
import (
"reflect"
"strings"
"github.com/containous/traefik/pkg/config/parser"
)
func getFlagTypes(element interface{}) map[string]reflect.Kind {
ref := map[string]reflect.Kind{}
if element == nil {
return ref
}
tp := reflect.TypeOf(element).Elem()
addFlagType(ref, "", tp)
return ref
}
func addFlagType(ref map[string]reflect.Kind, name string, typ reflect.Type) {
switch typ.Kind() {
case reflect.Bool, reflect.Slice:
ref[name] = typ.Kind()
case reflect.Map:
addFlagType(ref, getName(name, parser.MapNamePlaceholder), typ.Elem())
case reflect.Ptr:
if typ.Elem().Kind() == reflect.Struct {
ref[name] = typ.Kind()
}
addFlagType(ref, name, typ.Elem())
case reflect.Struct:
for j := 0; j < typ.NumField(); j++ {
subField := typ.Field(j)
if !parser.IsExported(subField) {
continue
}
if subField.Anonymous {
addFlagType(ref, getName(name), subField.Type)
} else {
addFlagType(ref, getName(name, subField.Name), subField.Type)
}
}
default:
// noop
}
}
func getName(names ...string) string {
return strings.TrimPrefix(strings.ToLower(strings.Join(names, ".")), ".")
}

View file

@ -0,0 +1,226 @@
package flag
import (
"reflect"
"testing"
"github.com/containous/traefik/pkg/config/parser"
"github.com/stretchr/testify/assert"
)
func Test_getFlagTypes(t *testing.T) {
testCases := []struct {
desc string
element interface{}
expected map[string]reflect.Kind
}{
{
desc: "nil",
element: nil,
expected: map[string]reflect.Kind{},
},
{
desc: "no fields",
element: &struct {
}{},
expected: map[string]reflect.Kind{},
},
{
desc: "string field",
element: &struct {
Foo string
}{},
expected: map[string]reflect.Kind{},
},
{
desc: "bool field level 0",
element: &struct {
Foo bool
fii bool
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Bool,
},
},
{
desc: "bool field level 1",
element: &struct {
Foo struct {
Field bool
}
}{},
expected: map[string]reflect.Kind{
"foo.field": reflect.Bool,
},
},
{
desc: "bool field level 2",
element: &struct {
Foo *struct {
Fii *struct {
Field bool
}
}
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Ptr,
"foo.fii": reflect.Ptr,
"foo.fii.field": reflect.Bool,
},
},
{
desc: "pointer field",
element: &struct {
Foo *struct {
Field string
}
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Ptr,
},
},
{
desc: "bool field level 3",
element: &struct {
Foo *struct {
Fii *struct {
Fuu *struct {
Field bool
}
}
}
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Ptr,
"foo.fii": reflect.Ptr,
"foo.fii.fuu": reflect.Ptr,
"foo.fii.fuu.field": reflect.Bool,
},
},
{
desc: "map string",
element: &struct {
Foo map[string]string
}{},
expected: map[string]reflect.Kind{},
},
{
desc: "map bool",
element: &struct {
Foo map[string]bool
Fii struct{}
}{},
expected: map[string]reflect.Kind{
"foo." + parser.MapNamePlaceholder: reflect.Bool,
},
},
{
desc: "map struct",
element: &struct {
Foo map[string]struct {
Field bool
}
}{},
expected: map[string]reflect.Kind{
"foo." + parser.MapNamePlaceholder + ".field": reflect.Bool,
},
},
{
desc: "map map bool",
element: &struct {
Foo map[string]map[string]bool
}{},
expected: map[string]reflect.Kind{
"foo." + parser.MapNamePlaceholder + "." + parser.MapNamePlaceholder: reflect.Bool,
},
},
{
desc: "map struct map",
element: &struct {
Foo map[string]struct {
Fii map[string]bool
}
}{},
expected: map[string]reflect.Kind{
"foo." + parser.MapNamePlaceholder + ".fii." + parser.MapNamePlaceholder: reflect.Bool,
},
},
{
desc: "pointer bool field level 0",
element: &struct {
Foo *bool
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Bool,
},
},
{
desc: "pointer int field level 0",
element: &struct {
Foo *int
}{},
expected: map[string]reflect.Kind{},
},
{
desc: "bool slice field level 0",
element: &struct {
Foo []bool
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Slice,
},
},
{
desc: "string slice field level 0",
element: &struct {
Foo []string
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Slice,
},
},
{
desc: "slice field level 1",
element: &struct {
Foo struct {
Field []string
}
}{},
expected: map[string]reflect.Kind{
"foo.field": reflect.Slice,
},
},
{
desc: "map slice string",
element: &struct {
Foo map[string][]string
}{},
expected: map[string]reflect.Kind{
"foo." + parser.MapNamePlaceholder: reflect.Slice,
},
},
{
desc: "embedded struct",
element: &struct {
Yo
}{},
expected: map[string]reflect.Kind{
"foo": reflect.Bool,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getFlagTypes(test.element)
assert.Equal(t, test.expected, actual)
})
}
}
type Yo struct {
Foo bool
}

View file

@ -0,0 +1,97 @@
// Package generator implements the custom initialization of all the fields of an empty interface.
package generator
import (
"reflect"
"github.com/containous/traefik/pkg/config/parser"
)
type initializer interface {
SetDefaults()
}
// Generate recursively initializes an empty structure, calling SetDefaults on each field, when it applies.
func Generate(element interface{}) {
if element == nil {
return
}
generate(element)
}
func generate(element interface{}) {
field := reflect.ValueOf(element)
fill(field)
}
func fill(field reflect.Value) {
switch field.Kind() {
case reflect.Ptr:
setPtr(field)
case reflect.Struct:
setStruct(field)
case reflect.Map:
setMap(field)
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.Struct ||
field.Type().Elem().Kind() == reflect.Ptr && field.Type().Elem().Elem().Kind() == reflect.Struct {
slice := reflect.MakeSlice(field.Type(), 1, 1)
field.Set(slice)
// use Ptr to allow "SetDefaults"
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
setPtr(value)
elem := value.Elem().Elem()
field.Index(0).Set(elem)
} else if field.Len() == 0 {
slice := reflect.MakeSlice(field.Type(), 0, 0)
field.Set(slice)
}
}
}
func setPtr(field reflect.Value) {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
if field.Type().Implements(reflect.TypeOf((*initializer)(nil)).Elem()) {
method := field.MethodByName("SetDefaults")
if method.IsValid() {
method.Call([]reflect.Value{})
}
}
fill(field.Elem())
}
func setStruct(field reflect.Value) {
for i := 0; i < field.NumField(); i++ {
fd := field.Field(i)
structField := field.Type().Field(i)
if structField.Tag.Get(parser.TagLabel) == "-" {
continue
}
if parser.IsExported(structField) {
fill(fd)
}
}
}
func setMap(field reflect.Value) {
if field.IsNil() {
field.Set(reflect.MakeMap(field.Type()))
}
ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem()))
fill(ptrValue)
value := ptrValue.Elem().Elem()
key := reflect.ValueOf(parser.MapNamePlaceholder)
field.SetMapIndex(key, value)
}

View file

@ -0,0 +1,439 @@
package generator
import (
"testing"
"github.com/containous/traefik/pkg/config/parser"
"github.com/stretchr/testify/assert"
)
func TestGenerate(t *testing.T) {
testCases := []struct {
desc string
element interface{}
expected interface{}
}{
{
desc: "nil",
},
{
desc: "simple",
element: &Ya{},
expected: &Ya{
Foo: &Yaa{
FieldIn1: "",
FieldIn2: false,
FieldIn3: 0,
FieldIn4: map[string]string{
parser.MapNamePlaceholder: "",
},
FieldIn5: map[string]int{
parser.MapNamePlaceholder: 0,
},
FieldIn6: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
FieldIn7: map[string]struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
FieldIn8: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {},
},
FieldIn9: map[string]*struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
FieldIn10: struct{ Field string }{},
FieldIn11: &struct{ Field string }{},
FieldIn12: func(v string) *string { return &v }(""),
FieldIn13: func(v bool) *bool { return &v }(false),
FieldIn14: func(v int) *int { return &v }(0),
},
Field1: "",
Field2: false,
Field3: 0,
Field4: map[string]string{
parser.MapNamePlaceholder: "",
},
Field5: map[string]int{
parser.MapNamePlaceholder: 0,
},
Field6: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
Field7: map[string]struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
Field8: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {},
},
Field9: map[string]*struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
Field10: struct{ Field string }{},
Field11: &struct{ Field string }{},
Field12: func(v string) *string { return &v }(""),
Field13: func(v bool) *bool { return &v }(false),
Field14: func(v int) *int { return &v }(0),
Field15: []int{},
},
},
{
desc: "with initial state",
element: &Ya{
Foo: &Yaa{
FieldIn1: "bar",
FieldIn2: false,
FieldIn3: 1,
FieldIn4: nil,
FieldIn5: nil,
FieldIn6: nil,
FieldIn7: nil,
FieldIn8: nil,
FieldIn9: nil,
FieldIn10: struct{ Field string }{},
FieldIn11: nil,
FieldIn12: nil,
FieldIn13: nil,
FieldIn14: nil,
},
Field1: "bir",
Field2: true,
Field3: 0,
Field4: nil,
Field5: nil,
Field6: nil,
Field7: nil,
Field8: nil,
Field9: nil,
Field10: struct{ Field string }{},
Field11: nil,
Field12: nil,
Field13: nil,
Field14: nil,
Field15: []int{7},
},
expected: &Ya{
Foo: &Yaa{
FieldIn1: "bar",
FieldIn2: false,
FieldIn3: 1,
FieldIn4: map[string]string{
parser.MapNamePlaceholder: "",
},
FieldIn5: map[string]int{
parser.MapNamePlaceholder: 0,
},
FieldIn6: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
FieldIn7: map[string]struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
FieldIn8: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {},
},
FieldIn9: map[string]*struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
FieldIn10: struct{ Field string }{},
FieldIn11: &struct{ Field string }{},
FieldIn12: func(v string) *string { return &v }(""),
FieldIn13: func(v bool) *bool { return &v }(false),
FieldIn14: func(v int) *int { return &v }(0),
},
Field1: "bir",
Field2: true,
Field3: 0,
Field4: map[string]string{
parser.MapNamePlaceholder: "",
},
Field5: map[string]int{
parser.MapNamePlaceholder: 0,
},
Field6: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
Field7: map[string]struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
Field8: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {},
},
Field9: map[string]*struct{ Field map[string]string }{
parser.MapNamePlaceholder: {
Field: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
Field10: struct{ Field string }{},
Field11: &struct{ Field string }{},
Field12: func(v string) *string { return &v }(""),
Field13: func(v bool) *bool { return &v }(false),
Field14: func(v int) *int { return &v }(0),
Field15: []int{7},
},
},
{
desc: "setDefault",
element: &Hu{},
expected: &Hu{
Foo: "hu",
Fii: &Hi{
Field: "hi",
},
Fuu: map[string]string{"<name>": ""},
Fee: map[string]Hi{"<name>": {Field: "hi"}},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
Generate(test.element)
assert.Equal(t, test.expected, test.element)
})
}
}
func Test_generate(t *testing.T) {
testCases := []struct {
desc string
element interface{}
expected interface{}
}{
{
desc: "struct pointer",
element: &struct {
Foo string
Fii *struct{ Field string }
}{},
expected: &struct {
Foo string
Fii *struct{ Field string }
}{
Foo: "",
Fii: &struct{ Field string }{
Field: "",
},
},
},
{
desc: "string slice",
element: &struct {
Foo []string
}{},
expected: &struct {
Foo []string
}{
Foo: []string{},
},
},
{
desc: "int slice",
element: &struct {
Foo []int
}{},
expected: &struct {
Foo []int
}{
Foo: []int{},
},
},
{
desc: "struct slice",
element: &struct {
Foo []struct {
Field string
}
}{},
expected: &struct {
Foo []struct {
Field string
}
}{
Foo: []struct {
Field string
}{
{Field: ""},
},
},
},
{
desc: "map string",
element: &struct {
Foo string
Fii map[string]string
}{},
expected: &struct {
Foo string
Fii map[string]string
}{
Foo: "",
Fii: map[string]string{
parser.MapNamePlaceholder: "",
},
},
},
{
desc: "map struct",
element: &struct {
Foo string
Fii map[string]struct{ Field string }
}{},
expected: &struct {
Foo string
Fii map[string]struct{ Field string }
}{
Foo: "",
Fii: map[string]struct{ Field string }{
parser.MapNamePlaceholder: {},
},
},
},
{
desc: "map struct pointer level 2",
element: &struct {
Foo string
Fuu *struct {
Fii map[string]*struct{ Field string }
}
}{},
expected: &struct {
Foo string
Fuu *struct {
Fii map[string]*struct{ Field string }
}
}{
Foo: "",
Fuu: &struct {
Fii map[string]*struct {
Field string
}
}{
Fii: map[string]*struct{ Field string }{
parser.MapNamePlaceholder: {
Field: "",
},
},
},
},
},
{
desc: "SetDefaults",
element: &Hu{},
expected: &Hu{
Foo: "hu",
Fii: &Hi{
Field: "hi",
},
Fuu: map[string]string{
parser.MapNamePlaceholder: "",
},
Fee: map[string]Hi{
parser.MapNamePlaceholder: {
Field: "hi",
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
generate(test.element)
assert.Equal(t, test.expected, test.element)
})
}
}
type Hu struct {
Foo string
Fii *Hi
Fuu map[string]string
Fee map[string]Hi
}
func (h *Hu) SetDefaults() {
h.Foo = "hu"
}
type Hi struct {
Field string
}
func (h *Hi) SetDefaults() {
h.Field = "hi"
}
type Ya struct {
Foo *Yaa
Field1 string
Field2 bool
Field3 int
Field4 map[string]string
Field5 map[string]int
Field6 map[string]struct{ Field string }
Field7 map[string]struct{ Field map[string]string }
Field8 map[string]*struct{ Field string }
Field9 map[string]*struct{ Field map[string]string }
Field10 struct{ Field string }
Field11 *struct{ Field string }
Field12 *string
Field13 *bool
Field14 *int
Field15 []int
}
type Yaa struct {
FieldIn1 string
FieldIn2 bool
FieldIn3 int
FieldIn4 map[string]string
FieldIn5 map[string]int
FieldIn6 map[string]struct{ Field string }
FieldIn7 map[string]struct{ Field map[string]string }
FieldIn8 map[string]*struct{ Field string }
FieldIn9 map[string]*struct{ Field map[string]string }
FieldIn10 struct{ Field string }
FieldIn11 *struct{ Field string }
FieldIn12 *string
FieldIn13 *bool
FieldIn14 *int
}

33
pkg/config/label/label.go Normal file
View file

@ -0,0 +1,33 @@
// Package label implements the decoding and encoding between flat labels and a typed Configuration.
package label
import (
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/config/parser"
)
// DecodeConfiguration converts the labels to a configuration.
func DecodeConfiguration(labels map[string]string) (*config.Configuration, error) {
conf := &config.Configuration{
HTTP: &config.HTTPConfiguration{},
TCP: &config.TCPConfiguration{},
}
err := parser.Decode(labels, conf, "traefik.http", "traefik.tcp")
if err != nil {
return nil, err
}
return conf, nil
}
// EncodeConfiguration converts a configuration to labels.
func EncodeConfiguration(conf *config.Configuration) (map[string]string, error) {
return parser.Encode(conf)
}
// Decode converts the labels to an element.
// labels -> [ node -> node + metadata (type) ] -> element (node)
func Decode(labels map[string]string, element interface{}, filters ...string) error {
return parser.Decode(labels, element, filters...)
}

View file

@ -5,8 +5,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -306,12 +306,12 @@ func TestDecodeConfiguration(t *testing.T) {
RateLimit: &config.RateLimit{ RateLimit: &config.RateLimit{
RateSet: map[string]*config.Rate{ RateSet: map[string]*config.Rate{
"Rate0": { "Rate0": {
Period: parse.Duration(42 * time.Second), Period: types.Duration(42 * time.Second),
Average: 42, Average: 42,
Burst: 42, Burst: 42,
}, },
"Rate1": { "Rate1": {
Period: parse.Duration(42 * time.Second), Period: types.Duration(42 * time.Second),
Average: 42, Average: 42,
Burst: 42, Burst: 42,
}, },
@ -700,12 +700,12 @@ func TestEncodeConfiguration(t *testing.T) {
RateLimit: &config.RateLimit{ RateLimit: &config.RateLimit{
RateSet: map[string]*config.Rate{ RateSet: map[string]*config.Rate{
"Rate0": { "Rate0": {
Period: parse.Duration(42 * time.Nanosecond), Period: types.Duration(42 * time.Nanosecond),
Average: 42, Average: 42,
Burst: 42, Burst: 42,
}, },
"Rate1": { "Rate1": {
Period: parse.Duration(42 * time.Nanosecond), Period: types.Duration(42 * time.Nanosecond),
Average: 42, Average: 42,
Burst: 42, Burst: 42,
}, },

View file

@ -1,8 +1,8 @@
package config package config
import ( import (
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/ip" "github.com/containous/traefik/pkg/ip"
"github.com/containous/traefik/pkg/types"
) )
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
@ -52,7 +52,7 @@ type Auth struct {
// BasicAuth holds the HTTP basic authentication configuration. // BasicAuth holds the HTTP basic authentication configuration.
type BasicAuth struct { type BasicAuth struct {
Users `json:"users,omitempty" mapstructure:","` Users Users `json:"users,omitempty"`
UsersFile string `json:"usersFile,omitempty"` UsersFile string `json:"usersFile,omitempty"`
Realm string `json:"realm,omitempty"` Realm string `json:"realm,omitempty"`
RemoveHeader bool `json:"removeHeader,omitempty"` RemoveHeader bool `json:"removeHeader,omitempty"`
@ -93,7 +93,7 @@ type Compress struct{}
// DigestAuth holds the Digest HTTP authentication configuration. // DigestAuth holds the Digest HTTP authentication configuration.
type DigestAuth struct { type DigestAuth struct {
Users `json:"users,omitempty" mapstructure:","` Users Users `json:"users,omitempty"`
UsersFile string `json:"usersFile,omitempty"` UsersFile string `json:"usersFile,omitempty"`
RemoveHeader bool `json:"removeHeader,omitempty"` RemoveHeader bool `json:"removeHeader,omitempty"`
Realm string `json:"realm,omitempty" mapstructure:","` Realm string `json:"realm,omitempty" mapstructure:","`
@ -273,7 +273,7 @@ type PassTLSClientCert struct {
// Rate holds the rate limiting configuration for a specific time period. // Rate holds the rate limiting configuration for a specific time period.
type Rate struct { type Rate struct {
Period parse.Duration `json:"period,omitempty"` Period types.Duration `json:"period,omitempty"`
Average int64 `json:"average,omitempty"` Average int64 `json:"average,omitempty"`
Burst int64 `json:"burst,omitempty"` Burst int64 `json:"burst,omitempty"`
} }

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"fmt" "fmt"
@ -7,15 +7,14 @@ import (
"strings" "strings"
"time" "time"
"github.com/containous/flaeg/parse" "github.com/containous/traefik/pkg/types"
) )
type initializer interface { type initializer interface {
SetDefaults() SetDefaults()
} }
// Fill the fields of the element. // Fill populates the fields of the element using the information in node.
// nodes -> element
func Fill(element interface{}, node *Node) error { func Fill(element interface{}, node *Node) error {
if element == nil || node == nil { if element == nil || node == nil {
return nil return nil
@ -25,12 +24,12 @@ func Fill(element interface{}, node *Node) error {
return fmt.Errorf("missing node type: %s", node.Name) return fmt.Errorf("missing node type: %s", node.Name)
} }
elem := reflect.ValueOf(element) root := reflect.ValueOf(element)
if elem.Kind() == reflect.Struct { if root.Kind() == reflect.Struct {
return fmt.Errorf("struct are not supported, use pointer instead") return fmt.Errorf("struct are not supported, use pointer instead")
} }
return fill(elem.Elem(), node) return fill(root.Elem(), node)
} }
func fill(field reflect.Value, node *Node) error { func fill(field reflect.Value, node *Node) error {
@ -117,8 +116,9 @@ func setStruct(field reflect.Value, node *Node) error {
} }
func setSlice(field reflect.Value, node *Node) error { func setSlice(field reflect.Value, node *Node) error {
if field.Type().Elem().Kind() == reflect.Struct { if field.Type().Elem().Kind() == reflect.Struct ||
return setSliceAsStruct(field, node) field.Type().Elem().Kind() == reflect.Ptr && field.Type().Elem().Elem().Kind() == reflect.Struct {
return setSliceStruct(field, node)
} }
if len(node.Value) == 0 { if len(node.Value) == 0 {
@ -135,7 +135,7 @@ func setSlice(field reflect.Value, node *Node) error {
switch field.Type().Elem().Kind() { switch field.Type().Elem().Kind() {
case reflect.String: case reflect.String:
field.Index(i).Set(reflect.ValueOf(value)) field.Index(i).SetString(value)
case reflect.Int: case reflect.Int:
val, err := strconv.ParseInt(value, 10, 64) val, err := strconv.ParseInt(value, 10, 64)
if err != nil { if err != nil {
@ -211,6 +211,27 @@ func setSlice(field reflect.Value, node *Node) error {
return nil return nil
} }
func setSliceStruct(field reflect.Value, node *Node) error {
if node.Tag.Get(TagLabelSliceAsStruct) != "" {
return setSliceAsStruct(field, node)
}
field.Set(reflect.MakeSlice(field.Type(), len(node.Children), len(node.Children)))
for i, child := range node.Children {
// use Ptr to allow "SetDefaults"
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
err := setPtr(value, child)
if err != nil {
return err
}
field.Index(i).Set(value.Elem().Elem())
}
return nil
}
func setSliceAsStruct(field reflect.Value, node *Node) error { func setSliceAsStruct(field reflect.Value, node *Node) error {
if len(node.Children) == 0 { if len(node.Children) == 0 {
return fmt.Errorf("invalid slice: node %s", node.Name) return fmt.Errorf("invalid slice: node %s", node.Name)
@ -254,7 +275,7 @@ func setMap(field reflect.Value, node *Node) error {
func setInt(field reflect.Value, value string, bitSize int) error { func setInt(field reflect.Value, value string, bitSize int) error {
switch field.Type() { switch field.Type() {
case reflect.TypeOf(parse.Duration(0)): case reflect.TypeOf(types.Duration(0)):
return setDuration(field, value, bitSize, time.Second) return setDuration(field, value, bitSize, time.Second)
case reflect.TypeOf(time.Duration(0)): case reflect.TypeOf(time.Duration(0)):
return setDuration(field, value, bitSize, time.Nanosecond) return setDuration(field, value, bitSize, time.Nanosecond)

View file

@ -1,11 +1,11 @@
package internal package parser
import ( import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/containous/flaeg/parse" "github.com/containous/traefik/pkg/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -390,7 +390,7 @@ func TestFill(t *testing.T) {
expected: expected{element: &struct{ Foo time.Duration }{Foo: 4 * time.Nanosecond}}, expected: expected{element: &struct{ Foo time.Duration }{Foo: 4 * time.Nanosecond}},
}, },
{ {
desc: "parse.Duration with unit", desc: "types.Duration with unit",
node: &Node{ node: &Node{
Name: "traefik", Name: "traefik",
Kind: reflect.Struct, Kind: reflect.Struct,
@ -398,11 +398,11 @@ func TestFill(t *testing.T) {
{Name: "Foo", FieldName: "Foo", Value: "4s", Kind: reflect.Int64}, {Name: "Foo", FieldName: "Foo", Value: "4s", Kind: reflect.Int64},
}, },
}, },
element: &struct{ Foo parse.Duration }{}, element: &struct{ Foo types.Duration }{},
expected: expected{element: &struct{ Foo parse.Duration }{Foo: parse.Duration(4 * time.Second)}}, expected: expected{element: &struct{ Foo types.Duration }{Foo: types.Duration(4 * time.Second)}},
}, },
{ {
desc: "parse.Duration without unit", desc: "types.Duration without unit",
node: &Node{ node: &Node{
Name: "traefik", Name: "traefik",
Kind: reflect.Struct, Kind: reflect.Struct,
@ -410,8 +410,8 @@ func TestFill(t *testing.T) {
{Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64},
}, },
}, },
element: &struct{ Foo parse.Duration }{}, element: &struct{ Foo types.Duration }{},
expected: expected{element: &struct{ Foo parse.Duration }{Foo: parse.Duration(4 * time.Second)}}, expected: expected{element: &struct{ Foo types.Duration }{Foo: types.Duration(4 * time.Second)}},
}, },
{ {
desc: "bool", desc: "bool",
@ -722,6 +722,30 @@ func TestFill(t *testing.T) {
element: &struct{ Foo []string }{}, element: &struct{ Foo []string }{},
expected: expected{element: &struct{ Foo []string }{Foo: []string{"huu", "hii", "hoo"}}}, expected: expected{element: &struct{ Foo []string }{Foo: []string{"huu", "hii", "hoo"}}},
}, },
{
desc: "slice named type",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "huu,hii,hoo", Kind: reflect.Slice},
},
},
element: &struct{ Foo []NamedType }{},
expected: expected{element: &struct{ Foo []NamedType }{Foo: []NamedType{"huu", "hii", "hoo"}}},
},
{
desc: "slice named type int",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "1,2,3", Kind: reflect.Slice},
},
},
element: &struct{ Foo []NamedTypeInt }{},
expected: expected{element: &struct{ Foo []NamedTypeInt }{Foo: []NamedTypeInt{1, 2, 3}}},
},
{ {
desc: "empty slice", desc: "empty slice",
node: &Node{ node: &Node{
@ -1046,18 +1070,6 @@ func TestFill(t *testing.T) {
element: &struct{ Foo []bool }{}, element: &struct{ Foo []bool }{},
expected: expected{error: true}, expected: expected{error: true},
}, },
{
desc: "slice struct",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "huu", Kind: reflect.Slice},
},
},
element: &struct{ Foo []struct{ Fii string } }{},
expected: expected{error: true},
},
{ {
desc: "slice slice-as-struct", desc: "slice slice-as-struct",
node: &Node{ node: &Node{
@ -1068,6 +1080,7 @@ func TestFill(t *testing.T) {
Name: "Fii", Name: "Fii",
FieldName: "Foo", FieldName: "Foo",
Kind: reflect.Slice, Kind: reflect.Slice,
Tag: `label-slice-as-struct:"Fii"`,
Children: []*Node{ Children: []*Node{
{Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"},
{Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"},
@ -1098,6 +1111,47 @@ func TestFill(t *testing.T) {
}, },
}}, }},
}, },
{
desc: "slice slice-as-struct pointer",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{
Name: "Fii",
FieldName: "Foo",
Kind: reflect.Slice,
Tag: `label-slice-as-struct:"Fii"`,
Children: []*Node{
{Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"},
{Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"},
},
},
},
},
element: &struct {
Foo []*struct {
Bar string
Bir string
} `label-slice-as-struct:"Fii"`
}{},
expected: expected{element: &struct {
Foo []*struct {
Bar string
Bir string
} `label-slice-as-struct:"Fii"`
}{
Foo: []*struct {
Bar string
Bir string
}{
{
Bar: "haa",
Bir: "hii",
},
},
}},
},
{ {
desc: "slice slice-as-struct without children", desc: "slice slice-as-struct without children",
node: &Node{ node: &Node{
@ -1107,6 +1161,7 @@ func TestFill(t *testing.T) {
{ {
Name: "Fii", Name: "Fii",
FieldName: "Foo", FieldName: "Foo",
Tag: `label-slice-as-struct:"Fii"`,
Kind: reflect.Slice, Kind: reflect.Slice,
}, },
}, },
@ -1134,12 +1189,12 @@ func TestFill(t *testing.T) {
}}, }},
}}, }},
element: &struct { element: &struct {
Foo *initialledFoo Foo *InitializedFoo
}{}, }{},
expected: expected{element: &struct { expected: expected{element: &struct {
Foo *initialledFoo Foo *InitializedFoo
}{ }{
Foo: &initialledFoo{ Foo: &InitializedFoo{
Fii: "default", Fii: "default",
Fuu: "huu", Fuu: "huu",
}, },
@ -1170,6 +1225,164 @@ func TestFill(t *testing.T) {
}, },
}}, }},
}, },
{
desc: "int pointer",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Ptr},
},
},
element: &struct{ Foo *int }{},
expected: expected{element: &struct{ Foo *int }{Foo: func(v int) *int { return &v }(4)}},
},
{
desc: "bool pointer",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Ptr},
},
},
element: &struct{ Foo *bool }{},
expected: expected{element: &struct{ Foo *bool }{Foo: func(v bool) *bool { return &v }(true)}},
},
{
desc: "string pointer",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.Ptr},
},
},
element: &struct{ Foo *string }{},
expected: expected{element: &struct{ Foo *string }{Foo: func(v string) *string { return &v }("bar")}},
},
{
desc: "embedded",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{
Name: "Foo",
FieldName: "Foo",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String},
}},
}},
element: &struct {
Foo struct {
FiiFoo
}
}{},
expected: expected{element: &struct {
Foo struct {
FiiFoo
}
}{
Foo: struct {
FiiFoo
}{
FiiFoo: FiiFoo{
Fii: "",
Fuu: "huu",
},
},
}},
},
{
desc: "slice struct",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{
{Name: "[0]", Kind: reflect.Struct, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String},
}},
{Name: "[1]", Kind: reflect.Struct, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String},
}},
{Name: "[2]", Kind: reflect.Struct, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String},
}},
}},
},
},
element: &struct {
Foo []struct {
Field1 string
Field2 string
}
}{},
expected: expected{element: &struct {
Foo []struct {
Field1 string
Field2 string
}
}{
Foo: []struct {
Field1 string
Field2 string
}{
{Field1: "A", Field2: "A"},
{Field1: "B", Field2: "B"},
{Field1: "C", Field2: "C"},
},
}},
},
{
desc: "slice pointer struct",
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{
{Name: "[0]", Kind: reflect.Ptr, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String},
}},
{Name: "[1]", Kind: reflect.Ptr, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String},
}},
{Name: "[2]", Kind: reflect.Ptr, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String},
}},
}},
},
},
element: &struct {
Foo []*struct {
Field1 string
Field2 string
}
}{},
expected: expected{element: &struct {
Foo []*struct {
Field1 string
Field2 string
}
}{
Foo: []*struct {
Field1 string
Field2 string
}{
{Field1: "A", Field2: "A"},
{Field1: "B", Field2: "B"},
{Field1: "C", Field2: "C"},
},
}},
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -1188,12 +1401,15 @@ func TestFill(t *testing.T) {
} }
} }
type initialledFoo struct { type NamedType string
type NamedTypeInt int
type InitializedFoo struct {
Fii string Fii string
Fuu string Fuu string
} }
func (t *initialledFoo) SetDefaults() { func (t *InitializedFoo) SetDefaults() {
t.Fii = "default" t.Fii = "default"
} }
@ -1206,3 +1422,10 @@ func (t *wrongInitialledFoo) SetDefaults() error {
t.Fii = "default" t.Fii = "default"
return nil return nil
} }
type Bouya string
type FiiFoo struct {
Fii string
Fuu Bouya
}

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"fmt" "fmt"
@ -7,13 +7,15 @@ import (
"strings" "strings"
) )
// EncodeToNode Converts an element to a node. // EncodeToNode converts an element to a node.
// element -> nodes // element -> nodes
func EncodeToNode(element interface{}) (*Node, error) { func EncodeToNode(element interface{}, omitEmpty bool) (*Node, error) {
rValue := reflect.ValueOf(element) rValue := reflect.ValueOf(element)
node := &Node{Name: "traefik"} node := &Node{Name: "traefik"}
err := setNodeValue(node, rValue) encoder := encoderToNode{omitEmpty: omitEmpty}
err := encoder.setNodeValue(node, rValue)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -21,7 +23,11 @@ func EncodeToNode(element interface{}) (*Node, error) {
return node, nil return node, nil
} }
func setNodeValue(node *Node, rValue reflect.Value) error { type encoderToNode struct {
omitEmpty bool
}
func (e encoderToNode) setNodeValue(node *Node, rValue reflect.Value) error {
switch rValue.Kind() { switch rValue.Kind() {
case reflect.String: case reflect.String:
node.Value = rValue.String() node.Value = rValue.String()
@ -34,13 +40,13 @@ func setNodeValue(node *Node, rValue reflect.Value) error {
case reflect.Bool: case reflect.Bool:
node.Value = strconv.FormatBool(rValue.Bool()) node.Value = strconv.FormatBool(rValue.Bool())
case reflect.Struct: case reflect.Struct:
return setStructValue(node, rValue) return e.setStructValue(node, rValue)
case reflect.Ptr: case reflect.Ptr:
return setNodeValue(node, rValue.Elem()) return e.setNodeValue(node, rValue.Elem())
case reflect.Map: case reflect.Map:
return setMapValue(node, rValue) return e.setMapValue(node, rValue)
case reflect.Slice: case reflect.Slice:
return setSliceValue(node, rValue) return e.setSliceValue(node, rValue)
default: default:
// noop // noop
} }
@ -48,14 +54,14 @@ func setNodeValue(node *Node, rValue reflect.Value) error {
return nil return nil
} }
func setStructValue(node *Node, rValue reflect.Value) error { func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error {
rType := rValue.Type() rType := rValue.Type()
for i := 0; i < rValue.NumField(); i++ { for i := 0; i < rValue.NumField(); i++ {
field := rType.Field(i) field := rType.Field(i)
fieldValue := rValue.Field(i) fieldValue := rValue.Field(i)
if !isExported(field) { if !IsExported(field) {
continue continue
} }
@ -67,7 +73,7 @@ func setStructValue(node *Node, rValue reflect.Value) error {
return err return err
} }
if isSkippedField(field, fieldValue) { if e.isSkippedField(field, fieldValue) {
continue continue
} }
@ -76,19 +82,32 @@ func setStructValue(node *Node, rValue reflect.Value) error {
nodeName = field.Tag.Get(TagLabelSliceAsStruct) nodeName = field.Tag.Get(TagLabelSliceAsStruct)
} }
child := &Node{Name: nodeName, FieldName: field.Name} if field.Anonymous {
if err := e.setNodeValue(node, fieldValue); err != nil {
return err
}
continue
}
if err := setNodeValue(child, fieldValue); err != nil { child := &Node{Name: nodeName, FieldName: field.Name, Description: field.Tag.Get(TagDescription)}
if err := e.setNodeValue(child, fieldValue); err != nil {
return err return err
} }
if field.Type.Kind() == reflect.Ptr && len(child.Children) == 0 { if field.Type.Kind() == reflect.Ptr {
if field.Tag.Get(TagLabel) != "allowEmpty" { if field.Type.Elem().Kind() != reflect.Struct && fieldValue.IsNil() {
continue
}
if field.Type.Elem().Kind() == reflect.Struct && len(child.Children) == 0 {
if field.Tag.Get(TagLabel) != TagLabelAllowEmpty {
continue continue
} }
child.Value = "true" child.Value = "true"
} }
}
node.Children = append(node.Children, child) node.Children = append(node.Children, child)
} }
@ -96,28 +115,44 @@ func setStructValue(node *Node, rValue reflect.Value) error {
return nil return nil
} }
func setMapValue(node *Node, rValue reflect.Value) error { func (e encoderToNode) setMapValue(node *Node, rValue reflect.Value) error {
for _, key := range rValue.MapKeys() { for _, key := range rValue.MapKeys() {
child := &Node{Name: key.String(), FieldName: key.String()} child := &Node{Name: key.String(), FieldName: key.String()}
node.Children = append(node.Children, child) node.Children = append(node.Children, child)
if err := setNodeValue(child, rValue.MapIndex(key)); err != nil { if err := e.setNodeValue(child, rValue.MapIndex(key)); err != nil {
return err return err
} }
} }
return nil return nil
} }
func setSliceValue(node *Node, rValue reflect.Value) error { func (e encoderToNode) setSliceValue(node *Node, rValue reflect.Value) error {
// label-slice-as-struct // label-slice-as-struct
if rValue.Type().Elem().Kind() == reflect.Struct && !strings.EqualFold(node.Name, node.FieldName) { if rValue.Type().Elem().Kind() == reflect.Struct && !strings.EqualFold(node.Name, node.FieldName) {
if rValue.Len() > 1 { if rValue.Len() > 1 {
return fmt.Errorf("node %s has too many slice entries: %d", node.Name, rValue.Len()) return fmt.Errorf("node %s has too many slice entries: %d", node.Name, rValue.Len())
} }
if err := setNodeValue(node, rValue.Index(0)); err != nil { return e.setNodeValue(node, rValue.Index(0))
}
if rValue.Type().Elem().Kind() == reflect.Struct ||
rValue.Type().Elem().Kind() == reflect.Ptr && rValue.Type().Elem().Elem().Kind() == reflect.Struct {
for i := 0; i < rValue.Len(); i++ {
child := &Node{Name: "[" + strconv.Itoa(i) + "]"}
eValue := rValue.Index(i)
err := e.setNodeValue(child, eValue)
if err != nil {
return err return err
} }
node.Children = append(node.Children, child)
}
return nil
} }
var values []string var values []string
@ -145,8 +180,8 @@ func setSliceValue(node *Node, rValue reflect.Value) error {
return nil return nil
} }
func isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool { func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool {
if field.Type.Kind() == reflect.String && fieldValue.Len() == 0 { if e.omitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 {
return true return true
} }
@ -154,7 +189,12 @@ func isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool {
return true return true
} }
if (field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Map) && if e.omitEmpty && (field.Type.Kind() == reflect.Slice) &&
(fieldValue.IsNil() || fieldValue.Len() == 0) {
return true
}
if (field.Type.Kind() == reflect.Map) &&
(fieldValue.IsNil() || fieldValue.Len() == 0) { (fieldValue.IsNil() || fieldValue.Len() == 0) {
return true return true
} }

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"testing" "testing"
@ -18,6 +18,16 @@ func TestEncodeToNode(t *testing.T) {
element interface{} element interface{}
expected expected expected expected
}{ }{
{
desc: "Description",
element: struct {
Foo string `description:"text"`
}{Foo: "bar"},
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "bar", Description: "text"},
}},
},
},
{ {
desc: "string", desc: "string",
element: struct { element: struct {
@ -257,10 +267,16 @@ func TestEncodeToNode(t *testing.T) {
Fuu: "huu", Fuu: "huu",
}, },
}, },
expected: expected{error: true}, expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "Fii", FieldName: "Fii", Value: "hii"},
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
}},
}},
},
}, },
{ {
desc: "struct nil pointer", desc: "string nil pointer",
element: struct { element: struct {
Foo *struct { Foo *struct {
Fii *string Fii *string
@ -271,10 +287,64 @@ func TestEncodeToNode(t *testing.T) {
Fii *string Fii *string
Fuu string Fuu string
}{ }{
Fii: nil,
Fuu: "huu", Fuu: "huu",
}, },
}, },
expected: expected{error: true}, expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
}},
}},
},
},
{
desc: "int pointer",
element: struct {
Foo *struct {
Fii *int
Fuu int
}
}{
Foo: &struct {
Fii *int
Fuu int
}{
Fii: func(v int) *int { return &v }(6),
Fuu: 4,
},
},
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "Fii", FieldName: "Fii", Value: "6"},
{Name: "Fuu", FieldName: "Fuu", Value: "4"},
}},
}},
},
},
{
desc: "bool pointer",
element: struct {
Foo *struct {
Fii *bool
Fuu bool
}
}{
Foo: &struct {
Fii *bool
Fuu bool
}{
Fii: func(v bool) *bool { return &v }(true),
Fuu: true,
},
},
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "Fii", FieldName: "Fii", Value: "true"},
{Name: "Fuu", FieldName: "Fuu", Value: "true"},
}},
}},
},
}, },
{ {
desc: "struct nil struct pointer", desc: "struct nil struct pointer",
@ -545,6 +615,60 @@ func TestEncodeToNode(t *testing.T) {
}, },
expected: expected{error: true}, expected: expected{error: true},
}, },
{
desc: "slice of struct",
element: struct {
Foo []struct {
Field string
}
}{
Foo: []struct {
Field string
}{
{
Field: "bar",
},
{
Field: "bir",
},
},
},
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "[0]", Children: []*Node{
{Name: "Field", FieldName: "Field", Value: "bar"},
}},
{Name: "[1]", Children: []*Node{
{Name: "Field", FieldName: "Field", Value: "bir"},
}},
}},
}}},
},
{
desc: "slice of pointer of struct",
element: struct {
Foo []*struct {
Field string
}
}{
Foo: []*struct {
Field string
}{
{Field: "bar"},
{Field: "bir"},
},
},
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "[0]", Children: []*Node{
{Name: "Field", FieldName: "Field", Value: "bar"},
}},
{Name: "[1]", Children: []*Node{
{Name: "Field", FieldName: "Field", Value: "bir"},
}},
}},
}}},
},
{ {
desc: "empty slice", desc: "empty slice",
element: struct { element: struct {
@ -572,6 +696,26 @@ func TestEncodeToNode(t *testing.T) {
}, },
expected: expected{node: &Node{Name: "traefik"}}, expected: expected{node: &Node{Name: "traefik"}},
}, },
{
desc: "embedded",
element: struct {
Foo struct{ FiiFoo }
}{
Foo: struct{ FiiFoo }{
FiiFoo: FiiFoo{
Fii: "hii",
Fuu: "huu",
},
},
},
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
{Name: "Foo", FieldName: "Foo", Children: []*Node{
{Name: "Fii", FieldName: "Fii", Value: "hii"},
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
}},
}},
},
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -579,7 +723,7 @@ func TestEncodeToNode(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
node, err := EncodeToNode(test.element) node, err := EncodeToNode(test.element, true)
if test.expected.error { if test.expected.error {
require.Error(t, err) require.Error(t, err)

View file

@ -0,0 +1,166 @@
package parser
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/containous/traefik/pkg/types"
)
const defaultPtrValue = "false"
// FlatOpts holds options used when encoding to Flat.
type FlatOpts struct {
Case string // "lower" or "upper", defaults to "lower".
Separator string
SkipRoot bool
}
// Flat is a configuration item representation.
type Flat struct {
Name string
Description string
Default string
}
// EncodeToFlat encodes a node to a Flat representation.
// Even though the given node argument should have already been augmented with metadata such as kind,
// the element (and its type information) is still needed to treat remaining edge cases.
func EncodeToFlat(element interface{}, node *Node, opts FlatOpts) ([]Flat, error) {
if element == nil || node == nil {
return nil, nil
}
if node.Kind == 0 {
return nil, fmt.Errorf("missing node type: %s", node.Name)
}
elem := reflect.ValueOf(element)
if elem.Kind() == reflect.Struct {
return nil, fmt.Errorf("structs are not supported, use pointer instead")
}
encoder := encoderToFlat{FlatOpts: opts}
var entries []Flat
if encoder.SkipRoot {
for _, child := range node.Children {
field := encoder.getField(elem.Elem(), child)
entries = append(entries, encoder.createFlat(field, child.Name, child)...)
}
} else {
entries = encoder.createFlat(elem, strings.ToLower(node.Name), node)
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
return entries, nil
}
type encoderToFlat struct {
FlatOpts
}
func (e encoderToFlat) createFlat(field reflect.Value, name string, node *Node) []Flat {
var entries []Flat
if node.Kind != reflect.Map && node.Description != "-" {
if !(node.Kind == reflect.Ptr && len(node.Children) > 0) ||
(node.Kind == reflect.Ptr && node.Tag.Get("label") == TagLabelAllowEmpty) {
if node.Name[0] != '[' {
entries = append(entries, Flat{
Name: e.getName(name),
Description: node.Description,
Default: e.getNodeValue(e.getField(field, node), node),
})
}
}
}
for _, child := range node.Children {
if node.Kind == reflect.Map {
fChild := e.getField(field, child)
var v string
if child.Kind == reflect.Struct {
v = defaultPtrValue
} else {
v = e.getNodeValue(fChild, child)
}
if node.Description != "-" {
entries = append(entries, Flat{
Name: e.getName(name, child.Name),
Description: node.Description,
Default: v,
})
}
if child.Kind == reflect.Struct || child.Kind == reflect.Ptr {
for _, ch := range child.Children {
f := e.getField(fChild, ch)
n := e.getName(name, child.Name, ch.Name)
entries = append(entries, e.createFlat(f, n, ch)...)
}
}
} else {
f := e.getField(field, child)
n := e.getName(name, child.Name)
entries = append(entries, e.createFlat(f, n, child)...)
}
}
return entries
}
func (e encoderToFlat) getField(field reflect.Value, node *Node) reflect.Value {
switch field.Kind() {
case reflect.Struct:
return field.FieldByName(node.FieldName)
case reflect.Ptr:
if field.Elem().Kind() == reflect.Struct {
return field.Elem().FieldByName(node.FieldName)
}
return field.Elem()
case reflect.Map:
return field.MapIndex(reflect.ValueOf(node.FieldName))
default:
return field
}
}
func (e encoderToFlat) getNodeValue(field reflect.Value, node *Node) string {
if node.Kind == reflect.Ptr && len(node.Children) > 0 {
return defaultPtrValue
}
if field.Kind() == reflect.Int64 {
i, _ := strconv.ParseInt(node.Value, 10, 64)
switch field.Type() {
case reflect.TypeOf(types.Duration(time.Second)):
return strconv.Itoa(int(i) / int(time.Second))
case reflect.TypeOf(time.Second):
return time.Duration(i).String()
}
}
return node.Value
}
func (e encoderToFlat) getName(names ...string) string {
var name string
if names[len(names)-1][0] == '[' {
name = strings.Join(names, "")
} else {
name = strings.Join(names, e.Separator)
}
if strings.EqualFold(e.Case, "upper") {
return strings.ToUpper(name)
}
return strings.ToLower(name)
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"fmt" "fmt"
@ -6,42 +6,39 @@ import (
"strings" "strings"
) )
// DecodeToNode Converts the labels to a node. const labelRoot = "traefik"
// labels -> nodes
// DecodeToNode converts the labels to a tree of nodes.
// If any filters are present, labels which do not match the filters are skipped.
func DecodeToNode(labels map[string]string, filters ...string) (*Node, error) { func DecodeToNode(labels map[string]string, filters ...string) (*Node, error) {
var sortedKeys []string sortedKeys := sortKeys(labels, filters)
for key := range labels {
if len(filters) == 0 {
sortedKeys = append(sortedKeys, key)
continue
}
for _, filter := range filters {
if len(key) >= len(filter) && strings.EqualFold(key[:len(filter)], filter) {
sortedKeys = append(sortedKeys, key)
continue
}
}
}
sort.Strings(sortedKeys)
labelRoot := "traefik"
var node *Node var node *Node
for i, key := range sortedKeys { for i, key := range sortedKeys {
split := strings.Split(key, ".") split := strings.Split(key, ".")
if split[0] != labelRoot { if split[0] != labelRoot {
// TODO (@ldez): error or continue
return nil, fmt.Errorf("invalid label root %s", split[0]) return nil, fmt.Errorf("invalid label root %s", split[0])
} }
labelRoot = split[0] var parts []string
for _, v := range split {
if v[0] == '[' {
return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v)
}
if strings.HasSuffix(v, "]") && v[0] != '[' {
indexLeft := strings.Index(v, "[")
parts = append(parts, v[:indexLeft], v[indexLeft:])
} else {
parts = append(parts, v)
}
}
if i == 0 { if i == 0 {
node = &Node{} node = &Node{}
} }
decodeToNode(node, split, labels[key]) decodeToNode(node, parts, labels[key])
} }
return node, nil return node, nil
@ -76,3 +73,23 @@ func containsNode(nodes []*Node, name string) *Node {
} }
return nil return nil
} }
func sortKeys(labels map[string]string, filters []string) []string {
var sortedKeys []string
for key := range labels {
if len(filters) == 0 {
sortedKeys = append(sortedKeys, key)
continue
}
for _, filter := range filters {
if len(key) >= len(filter) && strings.EqualFold(key[:len(filter)], filter) {
sortedKeys = append(sortedKeys, key)
continue
}
}
}
sort.Strings(sortedKeys)
return sortedKeys
}

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"encoding/json" "encoding/json"
@ -177,6 +177,40 @@ func TestDecodeToNode(t *testing.T) {
}, },
}}, }},
}, },
{
desc: "several entries, slice syntax",
in: map[string]string{
"traefik.foo[0].aaa": "bar0",
"traefik.foo[0].bbb": "bur0",
"traefik.foo[1].aaa": "bar1",
"traefik.foo[1].bbb": "bur1",
},
expected: expected{node: &Node{
Name: "traefik",
Children: []*Node{
{Name: "foo", Children: []*Node{
{Name: "[0]", Children: []*Node{
{Name: "aaa", Value: "bar0"},
{Name: "bbb", Value: "bur0"},
}},
{Name: "[1]", Children: []*Node{
{Name: "aaa", Value: "bar1"},
{Name: "bbb", Value: "bur1"},
}},
}},
},
}},
},
{
desc: "several entries, invalid slice syntax",
in: map[string]string{
"traefik.foo.[0].aaa": "bar0",
"traefik.foo.[0].bbb": "bur0",
"traefik.foo.[1].aaa": "bar1",
"traefik.foo.[1].bbb": "bur1",
},
expected: expected{error: true},
},
} }
for _, test := range testCases { for _, test := range testCases {

View file

@ -1,4 +1,4 @@
package internal package parser
// EncodeNode Converts a node to labels. // EncodeNode Converts a node to labels.
// nodes -> labels // nodes -> labels
@ -14,7 +14,13 @@ func encodeNode(labels map[string]string, root string, node *Node) {
continue continue
} }
childName := root + "." + child.Name var sep string
if child.Name[0] != '[' {
sep = "."
}
childName := root + sep + child.Name
if len(child.Children) > 0 { if len(child.Children) > 0 {
encodeNode(labels, childName, child) encodeNode(labels, childName, child)
} else if len(child.Name) > 0 { } else if len(child.Name) > 0 {

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"testing" "testing"
@ -141,6 +141,30 @@ func TestEncodeNode(t *testing.T) {
"traefik.bar.ccc": "bir", "traefik.bar.ccc": "bir",
}, },
}, },
{
desc: "slice of struct syntax",
node: &Node{
Name: "traefik",
Children: []*Node{
{Name: "foo", Children: []*Node{
{Name: "[0]", Children: []*Node{
{Name: "aaa", Value: "bar0"},
{Name: "bbb", Value: "bur0"},
}},
{Name: "[1]", Children: []*Node{
{Name: "aaa", Value: "bar1"},
{Name: "bbb", Value: "bur1"},
}},
}},
},
},
expected: map[string]string{
"traefik.foo[0].aaa": "bar0",
"traefik.foo[0].bbb": "bur0",
"traefik.foo[1].aaa": "bar1",
"traefik.foo[1].bbb": "bur1",
},
},
} }
for _, test := range testCases { for _, test := range testCases {

18
pkg/config/parser/node.go Normal file
View file

@ -0,0 +1,18 @@
package parser
import "reflect"
// MapNamePlaceholder is the placeholder for the map name.
const MapNamePlaceholder = "<name>"
// Node is a label node.
type Node struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
FieldName string `json:"fieldName"`
Value string `json:"value,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Kind reflect.Kind `json:"kind,omitempty"`
Tag reflect.StructTag `json:"tag,omitempty"`
Children []*Node `json:"children,omitempty"`
}

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"errors" "errors"
@ -7,9 +7,8 @@ import (
"strings" "strings"
) )
// AddMetadata Adds metadata to a node. // AddMetadata adds metadata such as type, inferred from element, to a node.
// nodes + element -> nodes func AddMetadata(element interface{}, node *Node) error {
func AddMetadata(structure interface{}, node *Node) error {
if node == nil { if node == nil {
return nil return nil
} }
@ -18,16 +17,25 @@ func AddMetadata(structure interface{}, node *Node) error {
return fmt.Errorf("invalid node %s: no child", node.Name) return fmt.Errorf("invalid node %s: no child", node.Name)
} }
if structure == nil { if element == nil {
return errors.New("nil structure") return errors.New("nil structure")
} }
rootType := reflect.TypeOf(structure) rootType := reflect.TypeOf(element)
node.Kind = rootType.Kind() node.Kind = rootType.Kind()
return browseChildren(rootType, node) return browseChildren(rootType, node)
} }
func browseChildren(fType reflect.Type, node *Node) error {
for _, child := range node.Children {
if err := addMetadata(fType, child); err != nil {
return err
}
}
return nil
}
func addMetadata(rootType reflect.Type, node *Node) error { func addMetadata(rootType reflect.Type, node *Node) error {
rType := rootType rType := rootType
if rootType.Kind() == reflect.Ptr { if rootType.Kind() == reflect.Ptr {
@ -45,14 +53,15 @@ func addMetadata(rootType reflect.Type, node *Node) error {
fType := field.Type fType := field.Type
node.Kind = fType.Kind() node.Kind = fType.Kind()
node.Tag = field.Tag
if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct || if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct ||
fType.Kind() == reflect.Map { fType.Kind() == reflect.Map {
if len(node.Children) == 0 && field.Tag.Get(TagLabel) != "allowEmpty" { if len(node.Children) == 0 && field.Tag.Get(TagLabel) != TagLabelAllowEmpty {
return fmt.Errorf("node %s (type %s) must have children", node.Name, fType) return fmt.Errorf("%s cannot be a standalone element (type %s)", node.Name, fType)
} }
node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(TagLabel) == "allowEmpty" node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(TagLabel) == TagLabelAllowEmpty
} }
if len(node.Children) == 0 { if len(node.Children) == 0 {
@ -79,11 +88,20 @@ func addMetadata(rootType reflect.Type, node *Node) error {
return nil return nil
} }
// only for struct/Ptr with label-slice-as-struct tag
if fType.Kind() == reflect.Slice { if fType.Kind() == reflect.Slice {
if field.Tag.Get(TagLabelSliceAsStruct) != "" {
return browseChildren(fType.Elem(), node) return browseChildren(fType.Elem(), node)
} }
for _, ch := range node.Children {
ch.Kind = fType.Elem().Kind()
if err = browseChildren(fType.Elem(), ch); err != nil {
return err
}
}
return nil
}
return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind()) return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind())
} }
@ -96,31 +114,32 @@ func findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error)
fieldName = cField.Name fieldName = cField.Name
} }
if isExported(cField) && strings.EqualFold(fieldName, node.Name) { if IsExported(cField) {
if cField.Anonymous {
if cField.Type.Kind() == reflect.Struct {
structField, err := findTypedField(cField.Type, node)
if err != nil {
continue
}
return structField, nil
}
}
if strings.EqualFold(fieldName, node.Name) {
node.FieldName = cField.Name node.FieldName = cField.Name
return cField, nil return cField, nil
} }
} }
}
return reflect.StructField{}, fmt.Errorf("field not found, node: %s", node.Name) return reflect.StructField{}, fmt.Errorf("field not found, node: %s", node.Name)
} }
func browseChildren(fType reflect.Type, node *Node) error { // IsExported reports whether f is exported.
for _, child := range node.Children {
if err := addMetadata(fType, child); err != nil {
return err
}
}
return nil
}
// isExported return true is a struct field is exported, else false
// https://golang.org/pkg/reflect/#StructField // https://golang.org/pkg/reflect/#StructField
func isExported(f reflect.StructField) bool { func IsExported(f reflect.StructField) bool {
if f.PkgPath != "" && !f.Anonymous { return f.PkgPath == ""
return false
}
return true
} }
func isSupportedType(field reflect.StructField) error { func isSupportedType(field reflect.StructField) error {
@ -142,20 +161,15 @@ func isSupportedType(field reflect.StructField) error {
reflect.Uint64, reflect.Uint64,
reflect.Uintptr, reflect.Uintptr,
reflect.Float32, reflect.Float32,
reflect.Float64: reflect.Float64,
reflect.Struct,
reflect.Ptr:
return nil return nil
default: default:
if len(field.Tag.Get(TagLabelSliceAsStruct)) > 0 {
return nil
}
return fmt.Errorf("unsupported slice type: %v", fType) return fmt.Errorf("unsupported slice type: %v", fType)
} }
} }
if fType.Kind() == reflect.Ptr && fType.Elem().Kind() != reflect.Struct {
return fmt.Errorf("unsupported pointer type: %v", fType.Elem())
}
if fType.Kind() == reflect.Map && fType.Key().Kind() != reflect.String { if fType.Kind() == reflect.Map && fType.Key().Kind() != reflect.String {
return fmt.Errorf("unsupported map key type: %v", fType.Key()) return fmt.Errorf("unsupported map key type: %v", fType.Key())
} }

View file

@ -1,4 +1,4 @@
package internal package parser
import ( import (
"encoding/json" "encoding/json"
@ -122,19 +122,6 @@ func TestAddMetadata(t *testing.T) {
structure: struct{ Foo interf }{}, structure: struct{ Foo interf }{},
expected: expected{error: true}, expected: expected{error: true},
}, },
{
desc: "level 1, slice struct",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Value: "1,2"},
},
},
structure: struct {
Foo []struct{ Foo string }
}{},
expected: expected{error: true},
},
{ {
desc: "level 1, map string", desc: "level 1, map string",
tree: &Node{ tree: &Node{
@ -217,7 +204,57 @@ func TestAddMetadata(t *testing.T) {
structure: struct { structure: struct {
Foo *int Foo *int
}{}, }{},
expected: expected{error: true}, expected: expected{
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Ptr},
},
},
},
},
{
desc: "level 1, bool pointer",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Value: "0"},
},
},
structure: struct {
Foo *bool
}{},
expected: expected{
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Ptr},
},
},
},
},
{
desc: "level 1, string pointer",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Value: "0"},
},
},
structure: struct {
Foo *string
}{},
expected: expected{
node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Ptr},
},
},
},
}, },
{ {
desc: "level 1, 2 children with different types", desc: "level 1, 2 children with different types",
@ -385,6 +422,7 @@ func TestAddMetadata(t *testing.T) {
Name: "Fii", Name: "Fii",
FieldName: "Foo", FieldName: "Foo",
Kind: reflect.Slice, Kind: reflect.Slice,
Tag: reflect.StructTag(`label-slice-as-struct:"Fii"`),
Children: []*Node{ Children: []*Node{
{Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"},
{Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"},
@ -420,6 +458,7 @@ func TestAddMetadata(t *testing.T) {
Name: "Fii", Name: "Fii",
FieldName: "Foo", FieldName: "Foo",
Kind: reflect.Slice, Kind: reflect.Slice,
Tag: reflect.StructTag(`label-slice-as-struct:"Fii"`),
}, },
}, },
}}, }},
@ -446,7 +485,7 @@ func TestAddMetadata(t *testing.T) {
Name: "traefik", Name: "traefik",
Kind: reflect.Struct, Kind: reflect.Struct,
Children: []*Node{ Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Struct}, {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)},
}, },
}, },
}, },
@ -473,7 +512,7 @@ func TestAddMetadata(t *testing.T) {
Name: "traefik", Name: "traefik",
Kind: reflect.Struct, Kind: reflect.Struct,
Children: []*Node{ Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "TruE", Kind: reflect.Struct}, {Name: "Foo", FieldName: "Foo", Value: "TruE", Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)},
}, },
}, },
}, },
@ -500,7 +539,7 @@ func TestAddMetadata(t *testing.T) {
Name: "traefik", Name: "traefik",
Kind: reflect.Struct, Kind: reflect.Struct,
Children: []*Node{ Children: []*Node{
{Name: "Foo", FieldName: "Foo", Value: "false", Disabled: true, Kind: reflect.Struct}, {Name: "Foo", FieldName: "Foo", Value: "false", Disabled: true, Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)},
}, },
}, },
}, },
@ -535,6 +574,7 @@ func TestAddMetadata(t *testing.T) {
Value: "false", Value: "false",
Disabled: true, Disabled: true,
Kind: reflect.Struct, Kind: reflect.Struct,
Tag: reflect.StructTag(`label:"allowEmpty"`),
Children: []*Node{ Children: []*Node{
{Name: "Bar", FieldName: "Bar", Value: "hii", Kind: reflect.String}, {Name: "Bar", FieldName: "Bar", Value: "hii", Kind: reflect.String},
}, },
@ -777,6 +817,173 @@ func TestAddMetadata(t *testing.T) {
}, },
}, },
}, },
{
desc: "Slice struct",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Children: []*Node{
{Name: "[0]", Children: []*Node{
{Name: "Field1", Value: "A"},
{Name: "Field2", Value: "A"},
}},
{Name: "[1]", Children: []*Node{
{Name: "Field1", Value: "B"},
{Name: "Field2", Value: "B"},
}},
{Name: "[2]", Children: []*Node{
{Name: "Field1", Value: "C"},
{Name: "Field2", Value: "C"},
}},
}},
},
},
structure: struct {
Foo []struct {
Field1 string
Field2 string
}
}{},
expected: expected{node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{
{Name: "[0]", Kind: reflect.Struct, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String},
}},
{Name: "[1]", Kind: reflect.Struct, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String},
}},
{Name: "[2]", Kind: reflect.Struct, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String},
}},
}},
},
}},
},
{
desc: "Slice pointer struct",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Children: []*Node{
{Name: "[0]", Children: []*Node{
{Name: "Field1", Value: "A"},
{Name: "Field2", Value: "A"},
}},
{Name: "[1]", Children: []*Node{
{Name: "Field1", Value: "B"},
{Name: "Field2", Value: "B"},
}},
{Name: "[2]", Children: []*Node{
{Name: "Field1", Value: "C"},
{Name: "Field2", Value: "C"},
}},
}},
},
},
structure: struct {
Foo []*struct {
Field1 string
Field2 string
}
}{},
expected: expected{node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{
{Name: "[0]", Kind: reflect.Ptr, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String},
}},
{Name: "[1]", Kind: reflect.Ptr, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String},
}},
{Name: "[2]", Kind: reflect.Ptr, Children: []*Node{
{Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String},
{Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String},
}},
}},
},
}},
},
{
desc: "embedded",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Children: []*Node{
{Name: "Fii", Value: "bir"},
{Name: "Fuu", Value: "bur"},
}},
},
},
structure: struct {
Foo struct {
FiiFoo
}
}{},
expected: expected{node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{
{Name: "Fii", FieldName: "Fii", Value: "bir", Kind: reflect.String},
{Name: "Fuu", FieldName: "Fuu", Value: "bur", Kind: reflect.String},
}},
},
}},
},
{
desc: "embedded slice",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "MySliceType", Value: "foo,fii"},
},
},
structure: struct {
MySliceType
}{},
expected: expected{node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "MySliceType", FieldName: "MySliceType", Value: "foo,fii", Kind: reflect.Slice},
},
}},
},
{
desc: "embedded slice 2",
tree: &Node{
Name: "traefik",
Children: []*Node{
{Name: "Foo", Children: []*Node{
{Name: "MySliceType", Value: "foo,fii"},
}},
},
},
structure: struct {
Foo struct {
MySliceType
}
}{},
expected: expected{node: &Node{
Name: "traefik",
Kind: reflect.Struct,
Children: []*Node{
{Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{
{Name: "MySliceType", FieldName: "MySliceType", Value: "foo,fii", Kind: reflect.Slice},
}},
},
}},
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -800,3 +1007,5 @@ func TestAddMetadata(t *testing.T) {
}) })
} }
} }
type MySliceType []string

View file

@ -0,0 +1,38 @@
// Package parser implements decoding and encoding between a flat map of labels and a typed Configuration.
package parser
// Decode decodes the given map of labels into the given element.
// If any filters are present, labels which do not match the filters are skipped.
// The operation goes through three stages roughly summarized as:
// labels -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element
func Decode(labels map[string]string, element interface{}, filters ...string) error {
node, err := DecodeToNode(labels, filters...)
if err != nil {
return err
}
err = AddMetadata(element, node)
if err != nil {
return err
}
err = Fill(element, node)
if err != nil {
return err
}
return nil
}
// Encode converts an element to labels.
// element -> node (value) -> label (node)
func Encode(element interface{}) (map[string]string, error) {
node, err := EncodeToNode(element, true)
if err != nil {
return nil, err
}
return EncodeNode(node), nil
}

18
pkg/config/parser/tags.go Normal file
View file

@ -0,0 +1,18 @@
package parser
const (
// TagLabel allows to apply a custom behavior.
// - "allowEmpty": allows to create an empty struct.
// - "-": ignore the field.
TagLabel = "label"
// TagLabelSliceAsStruct allows to use a slice of struct by creating one entry into the slice.
// The value is the substitution name used in the label to access the slice.
TagLabelSliceAsStruct = "label-slice-as-struct"
// TagDescription is the documentation for the field.
TagDescription = "description"
// TagLabelAllowEmpty is related to TagLabel.
TagLabelAllowEmpty = "allowEmpty"
)

View file

@ -1,30 +1,30 @@
package static package static
import (
"fmt"
"strings"
"github.com/containous/traefik/pkg/log"
)
// EntryPoint holds the entry point configuration. // EntryPoint holds the entry point configuration.
type EntryPoint struct { type EntryPoint struct {
Address string Address string `description:"Entry point address."`
Transport *EntryPointsTransport Transport *EntryPointsTransport `description:"Configures communication between clients and Traefik."`
ProxyProtocol *ProxyProtocol ProxyProtocol *ProxyProtocol `description:"Proxy-Protocol configuration." label:"allowEmpty"`
ForwardedHeaders *ForwardedHeaders ForwardedHeaders *ForwardedHeaders `description:"Trust client forwarding headers."`
}
// SetDefaults sets the default values.
func (e *EntryPoint) SetDefaults() {
e.Transport = &EntryPointsTransport{}
e.Transport.SetDefaults()
e.ForwardedHeaders = &ForwardedHeaders{}
} }
// ForwardedHeaders Trust client forwarding headers. // ForwardedHeaders Trust client forwarding headers.
type ForwardedHeaders struct { type ForwardedHeaders struct {
Insecure bool Insecure bool `description:"Trust all forwarded headers." export:"true"`
TrustedIPs []string TrustedIPs []string `description:"Trust only forwarded headers from selected IPs."`
} }
// ProxyProtocol contains Proxy-Protocol configuration. // ProxyProtocol contains Proxy-Protocol configuration.
type ProxyProtocol struct { type ProxyProtocol struct {
Insecure bool `export:"true"` Insecure bool `description:"Trust all." export:"true"`
TrustedIPs []string TrustedIPs []string `description:"Trust only selected IPs."`
} }
// EntryPoints holds the HTTP entry point list. // EntryPoints holds the HTTP entry point list.
@ -32,103 +32,14 @@ type EntryPoints map[string]*EntryPoint
// EntryPointsTransport configures communication between clients and Traefik. // EntryPointsTransport configures communication between clients and Traefik.
type EntryPointsTransport struct { type EntryPointsTransport struct {
LifeCycle *LifeCycle `description:"Timeouts influencing the server life cycle" export:"true"` LifeCycle *LifeCycle `description:"Timeouts influencing the server life cycle." export:"true"`
RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance" export:"true"` RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance." export:"true"`
} }
// String is the method to format the flag's value, part of the flag.Value interface. // SetDefaults sets the default values.
// The String method's output will be used in diagnostics. func (t *EntryPointsTransport) SetDefaults() {
func (ep EntryPoints) String() string { t.LifeCycle = &LifeCycle{}
return fmt.Sprintf("%+v", map[string]*EntryPoint(ep)) t.LifeCycle.SetDefaults()
} t.RespondingTimeouts = &RespondingTimeouts{}
t.RespondingTimeouts.SetDefaults()
// Get return the EntryPoints map.
func (ep *EntryPoints) Get() interface{} {
return *ep
}
// SetValue sets the EntryPoints map with val.
func (ep *EntryPoints) SetValue(val interface{}) {
*ep = val.(EntryPoints)
}
// Type is type of the struct.
func (ep *EntryPoints) Type() string {
return "entrypoints"
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (ep *EntryPoints) Set(value string) error {
result := parseEntryPointsConfiguration(value)
(*ep)[result["name"]] = &EntryPoint{
Address: result["address"],
ProxyProtocol: makeEntryPointProxyProtocol(result),
ForwardedHeaders: makeEntryPointForwardedHeaders(result),
}
return nil
}
func makeEntryPointProxyProtocol(result map[string]string) *ProxyProtocol {
var proxyProtocol *ProxyProtocol
ppTrustedIPs := result["proxyprotocol_trustedips"]
if len(result["proxyprotocol_insecure"]) > 0 || len(ppTrustedIPs) > 0 {
proxyProtocol = &ProxyProtocol{
Insecure: toBool(result, "proxyprotocol_insecure"),
}
if len(ppTrustedIPs) > 0 {
proxyProtocol.TrustedIPs = strings.Split(ppTrustedIPs, ",")
}
}
if proxyProtocol != nil && proxyProtocol.Insecure {
log.Warn("ProxyProtocol.insecure:true is dangerous. Please use 'ProxyProtocol.TrustedIPs:IPs' and remove 'ProxyProtocol.insecure:true'")
}
return proxyProtocol
}
func parseEntryPointsConfiguration(raw string) map[string]string {
sections := strings.Fields(raw)
config := make(map[string]string)
for _, part := range sections {
field := strings.SplitN(part, ":", 2)
name := strings.ToLower(strings.Replace(field[0], ".", "_", -1))
if len(field) > 1 {
config[name] = field[1]
} else {
if strings.EqualFold(name, "TLS") {
config["tls_acme"] = "TLS"
} else {
config[name] = ""
}
}
}
return config
}
func toBool(conf map[string]string, key string) bool {
if val, ok := conf[key]; ok {
return strings.EqualFold(val, "true") ||
strings.EqualFold(val, "enable") ||
strings.EqualFold(val, "on")
}
return false
}
func makeEntryPointForwardedHeaders(result map[string]string) *ForwardedHeaders {
forwardedHeaders := &ForwardedHeaders{}
forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure")
fhTrustedIPs := result["forwardedheaders_trustedips"]
if len(fhTrustedIPs) > 0 {
forwardedHeaders.TrustedIPs = strings.Split(fhTrustedIPs, ",")
}
return forwardedHeaders
} }

View file

@ -1,257 +0,0 @@
package static
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_parseEntryPointsConfiguration(t *testing.T) {
testCases := []struct {
name string
value string
expectedResult map[string]string
}{
{
name: "all parameters",
value: "Name:foo " +
"Address::8000 " +
"CA:car " +
"CA.Optional:true " +
"Redirect.EntryPoint:https " +
"Redirect.Regex:http://localhost/(.*) " +
"Redirect.Replacement:http://mydomain/$1 " +
"Redirect.Permanent:true " +
"Compress:true " +
"ProxyProtocol.TrustedIPs:192.168.0.1 " +
"ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 " +
"Auth.Basic.Realm:myRealm " +
"Auth.Basic.Users:test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 " +
"Auth.Basic.RemoveHeader:true " +
"Auth.Digest.Users:test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e " +
"Auth.Digest.RemoveHeader:true " +
"Auth.HeaderField:X-WebAuth-User " +
"Auth.Forward.Address:https://authserver.com/auth " +
"Auth.Forward.AuthResponseHeaders:X-Auth,X-Test,X-Secret " +
"Auth.Forward.TrustForwardHeader:true " +
"Auth.Forward.TLS.CA:path/to/local.crt " +
"Auth.Forward.TLS.CAOptional:true " +
"Auth.Forward.TLS.Cert:path/to/foo.cert " +
"Auth.Forward.TLS.Key:path/to/foo.key " +
"Auth.Forward.TLS.InsecureSkipVerify:true " +
"WhiteList.SourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"WhiteList.IPStrategy.depth:3 " +
"WhiteList.IPStrategy.ExcludedIPs:10.0.0.3/24,20.0.0.3/24 " +
"ClientIPStrategy.depth:3 " +
"ClientIPStrategy.ExcludedIPs:10.0.0.3/24,20.0.0.3/24 ",
expectedResult: map[string]string{
"address": ":8000",
"auth_basic_realm": "myRealm",
"auth_basic_users": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"auth_basic_removeheader": "true",
"auth_digest_users": "test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e",
"auth_digest_removeheader": "true",
"auth_forward_address": "https://authserver.com/auth",
"auth_forward_authresponseheaders": "X-Auth,X-Test,X-Secret",
"auth_forward_tls_ca": "path/to/local.crt",
"auth_forward_tls_caoptional": "true",
"auth_forward_tls_cert": "path/to/foo.cert",
"auth_forward_tls_insecureskipverify": "true",
"auth_forward_tls_key": "path/to/foo.key",
"auth_forward_trustforwardheader": "true",
"auth_headerfield": "X-WebAuth-User",
"ca": "car",
"ca_optional": "true",
"compress": "true",
"forwardedheaders_trustedips": "10.0.0.3/24,20.0.0.3/24",
"name": "foo",
"proxyprotocol_trustedips": "192.168.0.1",
"redirect_entrypoint": "https",
"redirect_permanent": "true",
"redirect_regex": "http://localhost/(.*)",
"redirect_replacement": "http://mydomain/$1",
"whitelist_sourcerange": "10.42.0.0/16,152.89.1.33/32,afed:be44::/16",
"whitelist_ipstrategy_depth": "3",
"whitelist_ipstrategy_excludedips": "10.0.0.3/24,20.0.0.3/24",
"clientipstrategy_depth": "3",
"clientipstrategy_excludedips": "10.0.0.3/24,20.0.0.3/24",
},
},
{
name: "compress on",
value: "name:foo Compress:on",
expectedResult: map[string]string{
"name": "foo",
"compress": "on",
},
},
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
conf := parseEntryPointsConfiguration(test.value)
assert.Len(t, conf, len(test.expectedResult))
assert.Equal(t, test.expectedResult, conf)
})
}
}
func Test_toBool(t *testing.T) {
testCases := []struct {
name string
value string
key string
expectedBool bool
}{
{
name: "on",
value: "on",
key: "foo",
expectedBool: true,
},
{
name: "true",
value: "true",
key: "foo",
expectedBool: true,
},
{
name: "enable",
value: "enable",
key: "foo",
expectedBool: true,
},
{
name: "arbitrary string",
value: "bar",
key: "foo",
expectedBool: false,
},
{
name: "no existing entry",
value: "bar",
key: "fii",
expectedBool: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
conf := map[string]string{
"foo": test.value,
}
result := toBool(conf, test.key)
assert.Equal(t, test.expectedBool, result)
})
}
}
func TestEntryPoints_Set(t *testing.T) {
testCases := []struct {
name string
expression string
expectedEntryPointName string
expectedEntryPoint *EntryPoint
}{
{
name: "all parameters camelcase",
expression: "Name:foo " +
"Address::8000 " +
"CA:car " +
"CA.Optional:true " +
"ProxyProtocol.TrustedIPs:192.168.0.1 ",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
Address: ":8000",
ProxyProtocol: &ProxyProtocol{
Insecure: false,
TrustedIPs: []string{"192.168.0.1"},
},
ForwardedHeaders: &ForwardedHeaders{},
// FIXME Test ServersTransport
},
},
{
name: "all parameters lowercase",
expression: "Name:foo " +
"address::8000 " +
"tls " +
"tls.minversion:VersionTLS11 " +
"tls.ciphersuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA " +
"ca:car " +
"ca.Optional:true " +
"proxyProtocol.TrustedIPs:192.168.0.1 ",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
Address: ":8000",
ProxyProtocol: &ProxyProtocol{
Insecure: false,
TrustedIPs: []string{"192.168.0.1"},
},
ForwardedHeaders: &ForwardedHeaders{},
// FIXME Test ServersTransport
},
},
{
name: "default",
expression: "Name:foo",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{},
},
},
{
name: "ProxyProtocol insecure true",
expression: "Name:foo ProxyProtocol.insecure:true",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ProxyProtocol: &ProxyProtocol{Insecure: true},
ForwardedHeaders: &ForwardedHeaders{},
},
},
{
name: "ProxyProtocol insecure false",
expression: "Name:foo ProxyProtocol.insecure:false",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ProxyProtocol: &ProxyProtocol{},
ForwardedHeaders: &ForwardedHeaders{},
},
},
{
name: "ProxyProtocol TrustedIPs",
expression: "Name:foo ProxyProtocol.TrustedIPs:10.0.0.3/24,20.0.0.3/24",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ProxyProtocol: &ProxyProtocol{
TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"},
},
ForwardedHeaders: &ForwardedHeaders{},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
eps := EntryPoints{}
err := eps.Set(test.expression)
require.NoError(t, err)
ep := eps[test.expectedEntryPointName]
assert.EqualValues(t, test.expectedEntryPoint, ep)
})
}
}

View file

@ -5,7 +5,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/ping" "github.com/containous/traefik/pkg/ping"
acmeprovider "github.com/containous/traefik/pkg/provider/acme" acmeprovider "github.com/containous/traefik/pkg/provider/acme"
@ -47,99 +46,127 @@ const (
type Configuration struct { type Configuration struct {
Global *Global `description:"Global configuration options" export:"true"` Global *Global `description:"Global configuration options" export:"true"`
ServersTransport *ServersTransport `description:"Servers default transport" export:"true"` ServersTransport *ServersTransport `description:"Servers default transport." export:"true"`
EntryPoints EntryPoints `description:"Entry points definition using format: --entryPoints='Name:http Address::8000' --entryPoints='Name:https Address::4442'" export:"true"` EntryPoints EntryPoints `description:"Entry points definition." export:"true"`
Providers *Providers `description:"Providers configuration" export:"true"` Providers *Providers `description:"Providers configuration." export:"true"`
API *API `description:"Enable api/dashboard" export:"true"` API *API `description:"Enable api/dashboard." export:"true" label:"allowEmpty"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"` Metrics *types.Metrics `description:"Enable a metrics exporter." export:"true"`
Ping *ping.Handler `description:"Enable ping" export:"true"` Ping *ping.Handler `description:"Enable ping." export:"true" label:"allowEmpty"`
// Rest *rest.Provider `description:"Enable Rest backend with default settings" export:"true"` // Rest *rest.Provider `description:"Enable Rest backend with default settings" export:"true"`
Log *types.TraefikLog `description:"Traefik log settings" export:"true"` Log *types.TraefikLog `description:"Traefik log settings." export:"true"`
AccessLog *types.AccessLog `description:"Access log settings" export:"true"` AccessLog *types.AccessLog `description:"Access log settings." export:"true" label:"allowEmpty"`
Tracing *Tracing `description:"OpenTracing configuration" export:"true"` Tracing *Tracing `description:"OpenTracing configuration." export:"true" label:"allowEmpty"`
HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening" export:"true"` HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening." export:"true" label:"allowEmpty"`
ACME *acmeprovider.Configuration `description:"Enable ACME (Let's Encrypt): automatic SSL" export:"true"` ACME *acmeprovider.Configuration `description:"Enable ACME (Let's Encrypt): automatic SSL." export:"true"`
} }
// Global holds the global configuration. // Global holds the global configuration.
type Global struct { type Global struct {
Debug bool `short:"d" description:"Enable debug mode" export:"true"` Debug bool `description:"Enable debug mode." export:"true"`
CheckNewVersion bool `description:"Periodically check if a new version has been released" export:"true"` CheckNewVersion bool `description:"Periodically check if a new version has been released." export:"true"`
SendAnonymousUsage *bool `description:"send periodically anonymous usage statistics" export:"true"` SendAnonymousUsage *bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default." export:"true"`
} }
// ServersTransport options to configure communication between Traefik and the servers // ServersTransport options to configure communication between Traefik and the servers
type ServersTransport struct { type ServersTransport struct {
InsecureSkipVerify bool `description:"Disable SSL certificate verification" export:"true"` InsecureSkipVerify bool `description:"Disable SSL certificate verification." export:"true"`
RootCAs tls.FilesOrContents `description:"Add cert file for self-signed certificate"` RootCAs []tls.FileOrContent `description:"Add cert file for self-signed certificate."`
MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" export:"true"` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" export:"true"`
ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers" export:"true"` ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers." export:"true"`
} }
// API holds the API configuration // API holds the API configuration
type API struct { type API struct {
EntryPoint string `description:"EntryPoint" export:"true"` EntryPoint string `description:"EntryPoint." export:"true"`
Dashboard bool `description:"Activate dashboard" export:"true"` Dashboard bool `description:"Activate dashboard." export:"true"`
Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"` Statistics *types.Statistics `description:"Enable more detailed statistics." export:"true" label:"allowEmpty"`
Middlewares []string `description:"Middleware list" export:"true"` Middlewares []string `description:"Middleware list." export:"true"`
DashboardAssets *assetfs.AssetFS `json:"-"` DashboardAssets *assetfs.AssetFS `json:"-" label:"-"`
}
// SetDefaults sets the default values.
func (a *API) SetDefaults() {
a.EntryPoint = "traefik"
a.Dashboard = true
} }
// RespondingTimeouts contains timeout configurations for incoming requests to the Traefik instance. // RespondingTimeouts contains timeout configurations for incoming requests to the Traefik instance.
type RespondingTimeouts struct { type RespondingTimeouts struct {
ReadTimeout parse.Duration `description:"ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set" export:"true"` ReadTimeout types.Duration `description:"ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set." export:"true"`
WriteTimeout parse.Duration `description:"WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set" export:"true"` WriteTimeout types.Duration `description:"WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set." export:"true"`
IdleTimeout parse.Duration `description:"IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. Defaults to 180 seconds. If zero, no timeout is set" export:"true"` IdleTimeout types.Duration `description:"IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set." export:"true"`
}
// SetDefaults sets the default values.
func (a *RespondingTimeouts) SetDefaults() {
a.IdleTimeout = types.Duration(DefaultIdleTimeout)
} }
// ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers. // ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers.
type ForwardingTimeouts struct { type ForwardingTimeouts struct {
DialTimeout parse.Duration `description:"The amount of time to wait until a connection to a backend server can be established. Defaults to 30 seconds. If zero, no timeout exists" export:"true"` DialTimeout types.Duration `description:"The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists." export:"true"`
ResponseHeaderTimeout parse.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"` ResponseHeaderTimeout types.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists." export:"true"`
}
// SetDefaults sets the default values.
func (f *ForwardingTimeouts) SetDefaults() {
f.DialTimeout = types.Duration(30 * time.Second)
} }
// LifeCycle contains configurations relevant to the lifecycle (such as the shutdown phase) of Traefik. // LifeCycle contains configurations relevant to the lifecycle (such as the shutdown phase) of Traefik.
type LifeCycle struct { type LifeCycle struct {
RequestAcceptGraceTimeout parse.Duration `description:"Duration to keep accepting requests before Traefik initiates the graceful shutdown procedure"` RequestAcceptGraceTimeout types.Duration `description:"Duration to keep accepting requests before Traefik initiates the graceful shutdown procedure."`
GraceTimeOut parse.Duration `description:"Duration to give active requests a chance to finish before Traefik stops"` GraceTimeOut types.Duration `description:"Duration to give active requests a chance to finish before Traefik stops."`
}
// SetDefaults sets the default values.
func (a *LifeCycle) SetDefaults() {
a.GraceTimeOut = types.Duration(DefaultGraceTimeout)
} }
// Tracing holds the tracing configuration. // Tracing holds the tracing configuration.
type Tracing struct { type Tracing struct {
Backend string `description:"Selects the tracking backend ('jaeger','zipkin','datadog','instana')." export:"true"` Backend string `description:"Selects the tracking backend ('jaeger','zipkin','datadog','instana')." export:"true"`
ServiceName string `description:"Set the name for this service" export:"true"` ServiceName string `description:"Set the name for this service." export:"true"`
SpanNameLimit int `description:"Set the maximum character limit for Span names (default 0 = no limit)" export:"true"` SpanNameLimit int `description:"Set the maximum character limit for Span names (default 0 = no limit)." export:"true"`
Jaeger *jaeger.Config `description:"Settings for jaeger"` Jaeger *jaeger.Config `description:"Settings for jaeger." label:"allowEmpty"`
Zipkin *zipkin.Config `description:"Settings for zipkin"` Zipkin *zipkin.Config `description:"Settings for zipkin." label:"allowEmpty"`
DataDog *datadog.Config `description:"Settings for DataDog"` DataDog *datadog.Config `description:"Settings for DataDog." label:"allowEmpty"`
Instana *instana.Config `description:"Settings for Instana"` Instana *instana.Config `description:"Settings for Instana." label:"allowEmpty"`
Haystack *haystack.Config `description:"Settings for Haystack"` Haystack *haystack.Config `description:"Settings for Haystack." label:"allowEmpty"`
}
// SetDefaults sets the default values.
func (t *Tracing) SetDefaults() {
t.Backend = "jaeger"
t.ServiceName = "traefik"
t.SpanNameLimit = 0
} }
// Providers contains providers configuration // Providers contains providers configuration
type Providers struct { type Providers struct {
ProvidersThrottleDuration parse.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." export:"true"` ProvidersThrottleDuration types.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." export:"true"`
Docker *docker.Provider `description:"Enable Docker backend with default settings" export:"true"` Docker *docker.Provider `description:"Enable Docker backend with default settings." export:"true" label:"allowEmpty"`
File *file.Provider `description:"Enable File backend with default settings" export:"true"` File *file.Provider `description:"Enable File backend with default settings." export:"true" label:"allowEmpty"`
Marathon *marathon.Provider `description:"Enable Marathon backend with default settings" export:"true"` Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." export:"true" label:"allowEmpty"`
Kubernetes *ingress.Provider `description:"Enable Kubernetes backend with default settings" export:"true"` Kubernetes *ingress.Provider `description:"Enable Kubernetes backend with default settings." export:"true" label:"allowEmpty"`
KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings" export:"true"` KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." export:"true" label:"allowEmpty"`
Rest *rest.Provider `description:"Enable Rest backend with default settings" export:"true"` Rest *rest.Provider `description:"Enable Rest backend with default settings." export:"true" label:"allowEmpty"`
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings" export:"true"` Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." export:"true" label:"allowEmpty"`
} }
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones. // SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
// It also takes care of maintaining backwards compatibility. // It also takes care of maintaining backwards compatibility.
func (c *Configuration) SetEffectiveConfiguration(configFile string) { func (c *Configuration) SetEffectiveConfiguration(configFile string) {
if len(c.EntryPoints) == 0 { if len(c.EntryPoints) == 0 {
ep := &EntryPoint{Address: ":80"}
ep.SetDefaults()
c.EntryPoints = EntryPoints{ c.EntryPoints = EntryPoints{
"http": &EntryPoint{ "http": ep,
Address: ":80",
},
} }
} }
@ -148,33 +175,15 @@ func (c *Configuration) SetEffectiveConfiguration(configFile string) {
(c.Metrics != nil && c.Metrics.Prometheus != nil && c.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) || (c.Metrics != nil && c.Metrics.Prometheus != nil && c.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) ||
(c.Providers.Rest != nil && c.Providers.Rest.EntryPoint == DefaultInternalEntryPointName) { (c.Providers.Rest != nil && c.Providers.Rest.EntryPoint == DefaultInternalEntryPointName) {
if _, ok := c.EntryPoints[DefaultInternalEntryPointName]; !ok { if _, ok := c.EntryPoints[DefaultInternalEntryPointName]; !ok {
c.EntryPoints[DefaultInternalEntryPointName] = &EntryPoint{Address: ":8080"} ep := &EntryPoint{Address: ":8080"}
} ep.SetDefaults()
} c.EntryPoints[DefaultInternalEntryPointName] = ep
for _, entryPoint := range c.EntryPoints {
if entryPoint.Transport == nil {
entryPoint.Transport = &EntryPointsTransport{}
}
// Make sure LifeCycle isn't nil to spare nil checks elsewhere.
if entryPoint.Transport.LifeCycle == nil {
entryPoint.Transport.LifeCycle = &LifeCycle{
GraceTimeOut: parse.Duration(DefaultGraceTimeout),
}
entryPoint.Transport.RespondingTimeouts = &RespondingTimeouts{
IdleTimeout: parse.Duration(DefaultIdleTimeout),
}
}
if entryPoint.ForwardedHeaders == nil {
entryPoint.ForwardedHeaders = &ForwardedHeaders{}
} }
} }
if c.Providers.Docker != nil { if c.Providers.Docker != nil {
if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 { if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 {
c.Providers.Docker.SwarmModeRefreshSeconds = 15 c.Providers.Docker.SwarmModeRefreshSeconds = types.Duration(15 * time.Second)
} }
} }

View file

@ -64,13 +64,8 @@ func initDatadogClient(ctx context.Context, config *types.Datadog) *time.Ticker
if len(address) == 0 { if len(address) == 0 {
address = "localhost:8125" address = "localhost:8125"
} }
pushInterval, err := time.ParseDuration(config.PushInterval)
if err != nil {
log.FromContext(ctx).Warnf("Unable to parse %s from config.PushInterval: using 10s as the default value", config.PushInterval)
pushInterval = 10 * time.Second
}
report := time.NewTicker(pushInterval) report := time.NewTicker(time.Duration(config.PushInterval))
safe.Go(func() { safe.Go(func() {
datadogClient.SendLoop(report.C, "udp", address) datadogClient.SendLoop(report.C, "udp", address)

View file

@ -16,7 +16,7 @@ func TestDatadog(t *testing.T) {
// This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond // This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond
udp.Timeout = 5 * time.Second udp.Timeout = 5 * time.Second
datadogRegistry := RegisterDatadog(context.Background(), &types.Datadog{Address: ":18125", PushInterval: "1s"}) datadogRegistry := RegisterDatadog(context.Background(), &types.Datadog{Address: ":18125", PushInterval: types.Duration(time.Second)})
defer StopDatadog() defer StopDatadog()
if !datadogRegistry.IsEnabled() { if !datadogRegistry.IsEnabled() {

View file

@ -51,7 +51,7 @@ func RegisterInfluxDB(ctx context.Context, config *types.InfluxDB) Registry {
influxDBClient = initInfluxDBClient(ctx, config) influxDBClient = initInfluxDBClient(ctx, config)
} }
if influxDBTicker == nil { if influxDBTicker == nil {
influxDBTicker = initInfluxDBTicker(ctx, config) influxDBTicker = initInfluxDBTicker(config)
} }
return &standardRegistry{ return &standardRegistry{
@ -115,14 +115,8 @@ func initInfluxDBClient(ctx context.Context, config *types.InfluxDB) *influx.Inf
} }
// initInfluxDBTicker initializes metrics pusher // initInfluxDBTicker initializes metrics pusher
func initInfluxDBTicker(ctx context.Context, config *types.InfluxDB) *time.Ticker { func initInfluxDBTicker(config *types.InfluxDB) *time.Ticker {
pushInterval, err := time.ParseDuration(config.PushInterval) report := time.NewTicker(time.Duration(config.PushInterval))
if err != nil {
log.FromContext(ctx).Warnf("Unable to parse %s from config.PushInterval: using 10s as the default value", config.PushInterval)
pushInterval = 10 * time.Second
}
report := time.NewTicker(pushInterval)
safe.Go(func() { safe.Go(func() {
var buf bytes.Buffer var buf bytes.Buffer

View file

@ -20,7 +20,7 @@ func TestInfluxDB(t *testing.T) {
// This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond // This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond
udp.Timeout = 5 * time.Second udp.Timeout = 5 * time.Second
influxDBRegistry := RegisterInfluxDB(context.Background(), &types.InfluxDB{Address: ":8089", PushInterval: "1s"}) influxDBRegistry := RegisterInfluxDB(context.Background(), &types.InfluxDB{Address: ":8089", PushInterval: types.Duration(time.Second)})
defer StopInfluxDB() defer StopInfluxDB()
if !influxDBRegistry.IsEnabled() { if !influxDBRegistry.IsEnabled() {
@ -80,7 +80,7 @@ func TestInfluxDBHTTP(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
influxDBRegistry := RegisterInfluxDB(context.Background(), &types.InfluxDB{Address: ts.URL, Protocol: "http", PushInterval: "1s", Database: "test", RetentionPolicy: "autogen"}) influxDBRegistry := RegisterInfluxDB(context.Background(), &types.InfluxDB{Address: ts.URL, Protocol: "http", PushInterval: types.Duration(time.Second), Database: "test", RetentionPolicy: "autogen"})
defer StopInfluxDB() defer StopInfluxDB()
if !influxDBRegistry.IsEnabled() { if !influxDBRegistry.IsEnabled() {

View file

@ -62,13 +62,8 @@ func initStatsdTicker(ctx context.Context, config *types.Statsd) *time.Ticker {
if len(address) == 0 { if len(address) == 0 {
address = "localhost:8125" address = "localhost:8125"
} }
pushInterval, err := time.ParseDuration(config.PushInterval)
if err != nil {
log.FromContext(ctx).Warnf("Unable to parse %s from config.PushInterval: using 10s as the default value", config.PushInterval)
pushInterval = 10 * time.Second
}
report := time.NewTicker(pushInterval) report := time.NewTicker(time.Duration(config.PushInterval))
safe.Go(func() { safe.Go(func() {
statsdClient.SendLoop(report.C, "udp", address) statsdClient.SendLoop(report.C, "udp", address)

View file

@ -15,7 +15,7 @@ func TestStatsD(t *testing.T) {
// This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond // This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond
udp.Timeout = 5 * time.Second udp.Timeout = 5 * time.Second
statsdRegistry := RegisterStatsd(context.Background(), &types.Statsd{Address: ":18125", PushInterval: "1s"}) statsdRegistry := RegisterStatsd(context.Background(), &types.Statsd{Address: ":18125", PushInterval: types.Duration(time.Second)})
defer StopStatsd() defer StopStatsd()
if !statsdRegistry.IsEnabled() { if !statsdRegistry.IsEnabled() {

View file

@ -13,7 +13,6 @@ import (
"time" "time"
"github.com/containous/alice" "github.com/containous/alice"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/types" "github.com/containous/traefik/pkg/types"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -330,7 +329,7 @@ func (h *Handler) keepAccessLog(statusCode, retryAttempts int, duration time.Dur
return true return true
} }
if h.config.Filters.MinDuration > 0 && (parse.Duration(duration) > h.config.Filters.MinDuration) { if h.config.Filters.MinDuration > 0 && (types.Duration(duration) > h.config.Filters.MinDuration) {
return true return true
} }

View file

@ -14,7 +14,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/types" "github.com/containous/traefik/pkg/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -287,12 +286,12 @@ func TestLoggerJSON(t *testing.T) {
Format: JSONFormat, Format: JSONFormat,
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldNames{ Names: map[string]string{
RequestHost: "keep", RequestHost: "keep",
}, },
Headers: &types.FieldHeaders{ Headers: &types.FieldHeaders{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldHeaderNames{ Names: map[string]string{
"Referer": "keep", "Referer": "keep",
}, },
}, },
@ -388,7 +387,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
FilePath: "", FilePath: "",
Format: CommonFormat, Format: CommonFormat,
Filters: &types.AccessLogFilters{ Filters: &types.AccessLogFilters{
MinDuration: parse.Duration(1 * time.Hour), MinDuration: types.Duration(1 * time.Hour),
}, },
}, },
expectedLog: ``, expectedLog: ``,
@ -399,7 +398,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
FilePath: "", FilePath: "",
Format: CommonFormat, Format: CommonFormat,
Filters: &types.AccessLogFilters{ Filters: &types.AccessLogFilters{
MinDuration: parse.Duration(1 * time.Millisecond), MinDuration: types.Duration(1 * time.Millisecond),
}, },
}, },
expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`, expectedLog: `TestHost - TestUser [13/Apr/2016:07:14:19 -0700] "POST testpath HTTP/0.0" 123 12 "testReferer" "testUserAgent" 23 "testRouter" "http://127.0.0.1/testService" 1ms`,
@ -433,7 +432,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
Format: CommonFormat, Format: CommonFormat,
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "keep", DefaultMode: "keep",
Names: types.FieldNames{ Names: map[string]string{
ClientHost: "drop", ClientHost: "drop",
}, },
}, },
@ -458,7 +457,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
Format: CommonFormat, Format: CommonFormat,
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldNames{ Names: map[string]string{
ClientHost: "drop", ClientHost: "drop",
ClientUsername: "keep", ClientUsername: "keep",
}, },
@ -473,7 +472,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
Format: CommonFormat, Format: CommonFormat,
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldNames{ Names: map[string]string{
ClientHost: "drop", ClientHost: "drop",
ClientUsername: "keep", ClientUsername: "keep",
}, },
@ -491,7 +490,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
Format: CommonFormat, Format: CommonFormat,
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldNames{ Names: map[string]string{
ClientHost: "drop", ClientHost: "drop",
ClientUsername: "keep", ClientUsername: "keep",
}, },
@ -509,13 +508,13 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
Format: CommonFormat, Format: CommonFormat,
Fields: &types.AccessLogFields{ Fields: &types.AccessLogFields{
DefaultMode: "drop", DefaultMode: "drop",
Names: types.FieldNames{ Names: map[string]string{
ClientHost: "drop", ClientHost: "drop",
ClientUsername: "keep", ClientUsername: "keep",
}, },
Headers: &types.FieldHeaders{ Headers: &types.FieldHeaders{
DefaultMode: "keep", DefaultMode: "keep",
Names: types.FieldHeaderNames{ Names: map[string]string{
"Referer": "redact", "Referer": "redact",
}, },
}, },

View file

@ -10,11 +10,16 @@ import (
// Handler expose ping routes. // Handler expose ping routes.
type Handler struct { type Handler struct {
EntryPoint string `description:"Ping entryPoint" export:"true"` EntryPoint string `description:"Ping entryPoint." export:"true"`
Middlewares []string `description:"Middleware list" export:"true"` Middlewares []string `description:"Middleware list." export:"true"`
terminating bool terminating bool
} }
// SetDefaults sets the default values.
func (h *Handler) SetDefaults() {
h.EntryPoint = "traefik"
}
// WithContext causes the ping endpoint to serve non 200 responses. // WithContext causes the ping endpoint to serve non 200 responses.
func (h *Handler) WithContext(ctx context.Context) { func (h *Handler) WithContext(ctx context.Context) {
go func() { go func() {

View file

@ -14,7 +14,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/rules" "github.com/containous/traefik/pkg/rules"
@ -39,17 +38,24 @@ var (
// Configuration holds ACME configuration provided by users // Configuration holds ACME configuration provided by users
type Configuration struct { type Configuration struct {
Email string `description:"Email address used for registration"` Email string `description:"Email address used for registration."`
ACMELogging bool `description:"Enable debug logging of ACME actions."` ACMELogging bool `description:"Enable debug logging of ACME actions."`
CAServer string `description:"CA server to use."` CAServer string `description:"CA server to use."`
Storage string `description:"Storage to use."` Storage string `description:"Storage to use."`
EntryPoint string `description:"EntryPoint to use."` EntryPoint string `description:"EntryPoint to use."`
KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'"` KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'."`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` OnHostRule bool `description:"Enable certificate generation on router Host rules."`
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"` DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." label:"allowEmpty"`
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge"` HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." label:"allowEmpty"`
TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge"` TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge." label:"allowEmpty"`
Domains []types.Domain `description:"CN and SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. Wildcard domains only accepted with DNSChallenge"` Domains []types.Domain `description:"The list of domains for which certificates are generated on startup. Wildcard domains only accepted with DNSChallenge."`
}
// SetDefaults sets the default values.
func (a *Configuration) SetDefaults() {
a.CAServer = lego.LEDirectoryProduction
a.Storage = "acme.json"
a.KeyType = "RSA4096"
} }
// Certificate is a struct which contains all data needed from an ACME certificate // Certificate is a struct which contains all data needed from an ACME certificate
@ -62,8 +68,8 @@ type Certificate struct {
// DNSChallenge contains DNS challenge Configuration // DNSChallenge contains DNS challenge Configuration
type DNSChallenge struct { type DNSChallenge struct {
Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."` Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."`
DelayBeforeCheck parse.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` DelayBeforeCheck types.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
Resolvers types.DNSResolvers `description:"Use following DNS servers to resolve the FQDN authority."` Resolvers []string `description:"Use following DNS servers to resolve the FQDN authority."`
DisablePropagationCheck bool `description:"Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended]"` DisablePropagationCheck bool `description:"Disable the DNS propagation checks before notifying ACME that the DNS challenge is ready. [not recommended]"`
} }
@ -239,7 +245,7 @@ func (p *Provider) getClient() (*lego.Client, error) {
logger.Debug("Building ACME client...") logger.Debug("Building ACME client...")
caServer := "https://acme-v02.api.letsencrypt.org/directory" caServer := lego.LEDirectoryProduction
if len(p.CAServer) > 0 { if len(p.CAServer) > 0 {
caServer = p.CAServer caServer = p.CAServer
} }

View file

@ -4,7 +4,7 @@ import "github.com/containous/traefik/pkg/types"
// Constrainer Filter services by constraint, matching with Traefik tags. // Constrainer Filter services by constraint, matching with Traefik tags.
type Constrainer struct { type Constrainer struct {
Constraints types.Constraints `description:"Filter services by constraint, matching with Traefik tags." export:"true"` Constraints []*types.Constraint `description:"Filter services by constraint, matching with Traefik tags." export:"true"`
} }
// MatchConstraints must match with EVERY single constraint // MatchConstraints must match with EVERY single constraint

View file

@ -8,9 +8,9 @@ import (
"strings" "strings"
"github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/config/label"
"github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/provider" "github.com/containous/traefik/pkg/provider"
"github.com/containous/traefik/pkg/provider/label"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
) )

View file

@ -339,7 +339,7 @@ func Test_buildConfiguration(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
containers []dockerData containers []dockerData
constraints types.Constraints constraints []*types.Constraint
expected *config.Configuration expected *config.Configuration
}{ }{
{ {
@ -1924,11 +1924,11 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
}, },
constraints: types.Constraints{ constraints: []*types.Constraint{
&types.Constraint{ {
Key: "tag", Key: "tag",
MustMatch: true, MustMatch: true,
Regex: "bar", Value: "bar",
}, },
}, },
expected: &config.Configuration{ expected: &config.Configuration{
@ -1965,11 +1965,11 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
}, },
constraints: types.Constraints{ constraints: []*types.Constraint{
&types.Constraint{ {
Key: "tag", Key: "tag",
MustMatch: true, MustMatch: true,
Regex: "foo", Value: "foo",
}, },
}, },
expected: &config.Configuration{ expected: &config.Configuration{

View file

@ -45,19 +45,29 @@ var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
provider.Constrainer `mapstructure:",squash" export:"true"` provider.Constrainer `description:"List of constraints used to filter out some containers." export:"true"`
Watch bool `description:"Watch provider" export:"true"` Watch bool `description:"Watch provider." export:"true"`
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"` Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint."`
DefaultRule string `description:"Default rule"` DefaultRule string `description:"Default rule."`
TLS *types.ClientTLS `description:"Enable Docker TLS support" export:"true"` TLS *types.ClientTLS `description:"Enable Docker TLS support." export:"true"`
ExposedByDefault bool `description:"Expose containers by default" export:"true"` ExposedByDefault bool `description:"Expose containers by default." export:"true"`
UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network" export:"true"` UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network." export:"true"`
SwarmMode bool `description:"Use Docker on Swarm Mode" export:"true"` SwarmMode bool `description:"Use Docker on Swarm Mode." export:"true"`
Network string `description:"Default Docker network used" export:"true"` Network string `description:"Default Docker network used." export:"true"`
SwarmModeRefreshSeconds int `description:"Polling interval for swarm mode (in seconds)" export:"true"` SwarmModeRefreshSeconds types.Duration `description:"Polling interval for swarm mode." export:"true"`
defaultRuleTpl *template.Template defaultRuleTpl *template.Template
} }
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.Watch = true
p.ExposedByDefault = true
p.Endpoint = "unix:///var/run/docker.sock"
p.SwarmMode = false
p.SwarmModeRefreshSeconds = types.Duration(15 * time.Second)
p.DefaultRule = DefaultTemplateRule
}
// Init the provider. // Init the provider.
func (p *Provider) Init() error { func (p *Provider) Init() error {
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
@ -184,7 +194,7 @@ func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.P
if p.SwarmMode { if p.SwarmMode {
errChan := make(chan error) errChan := make(chan error)
// TODO: This need to be change. Linked to Swarm events docker/docker#23827 // TODO: This need to be change. Linked to Swarm events docker/docker#23827
ticker := time.NewTicker(time.Second * time.Duration(p.SwarmModeRefreshSeconds)) ticker := time.NewTicker(time.Duration(p.SwarmModeRefreshSeconds))
pool.GoCtx(func(ctx context.Context) { pool.GoCtx(func(ctx context.Context) {
ctx = log.With(ctx, log.Str(log.ProviderName, "docker")) ctx = log.With(ctx, log.Str(log.ProviderName, "docker"))

View file

@ -3,7 +3,7 @@ package docker
import ( import (
"fmt" "fmt"
"github.com/containous/traefik/pkg/provider/label" "github.com/containous/traefik/pkg/config/label"
) )
const ( const (

View file

@ -28,11 +28,17 @@ var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
Directory string `description:"Load configuration from one or more .toml files in a directory" export:"true"` Directory string `description:"Load configuration from one or more .toml files in a directory." export:"true"`
Watch bool `description:"Watch provider" export:"true"` Watch bool `description:"Watch provider." export:"true"`
Filename string `description:"Override default configuration template. For advanced users :)" export:"true"` Filename string `description:"Override default configuration template. For advanced users :)" export:"true"`
DebugLogGeneratedTemplate bool `description:"Enable debug logging of generated configuration template." export:"true"` DebugLogGeneratedTemplate bool `description:"Enable debug logging of generated configuration template." export:"true"`
TraefikFile string TraefikFile string `description:"-"`
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.Watch = true
p.Filename = ""
} }
// Init the provider // Init the provider

View file

@ -224,7 +224,7 @@ func createProvider(t *testing.T, test ProvideTestCase, watch bool) (*Provider,
tempDir := createTempDir(t, "testdir") tempDir := createTempDir(t, "testdir")
provider := &Provider{} provider := &Provider{}
provider.Watch = watch provider.Watch = true
if len(test.directoryContent) > 0 { if len(test.directoryContent) > 0 {
if !watch { if !watch {

Some files were not shown because too many files have changed in this diff Show more