1
0
Fork 0

Dynamic Configuration Refactoring

This commit is contained in:
Ludovic Fernandez 2018-11-14 10:18:03 +01:00 committed by Traefiker Bot
parent d3ae88f108
commit a09dfa3ce1
452 changed files with 21023 additions and 9419 deletions

View file

@ -0,0 +1,445 @@
package configuration
import (
"fmt"
"strings"
"time"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/acme"
"github.com/containous/traefik/old/api"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/middlewares/tracing"
"github.com/containous/traefik/old/middlewares/tracing/datadog"
"github.com/containous/traefik/old/middlewares/tracing/jaeger"
"github.com/containous/traefik/old/middlewares/tracing/zipkin"
"github.com/containous/traefik/old/ping"
"github.com/containous/traefik/old/provider/boltdb"
"github.com/containous/traefik/old/provider/consul"
"github.com/containous/traefik/old/provider/consulcatalog"
"github.com/containous/traefik/old/provider/docker"
"github.com/containous/traefik/old/provider/dynamodb"
"github.com/containous/traefik/old/provider/ecs"
"github.com/containous/traefik/old/provider/etcd"
"github.com/containous/traefik/old/provider/eureka"
"github.com/containous/traefik/old/provider/file"
"github.com/containous/traefik/old/provider/kubernetes"
"github.com/containous/traefik/old/provider/marathon"
"github.com/containous/traefik/old/provider/mesos"
"github.com/containous/traefik/old/provider/rancher"
"github.com/containous/traefik/old/provider/rest"
"github.com/containous/traefik/old/provider/zk"
"github.com/containous/traefik/old/types"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/tls"
newtypes "github.com/containous/traefik/types"
"github.com/pkg/errors"
lego "github.com/xenolf/lego/acme"
)
const (
// DefaultInternalEntryPointName the name of the default internal entry point
DefaultInternalEntryPointName = "traefik"
// DefaultHealthCheckInterval is the default health check interval.
DefaultHealthCheckInterval = 30 * time.Second
// DefaultHealthCheckTimeout is the default health check request timeout.
DefaultHealthCheckTimeout = 5 * time.Second
// DefaultDialTimeout when connecting to a backend server.
DefaultDialTimeout = 30 * time.Second
// DefaultIdleTimeout before closing an idle connection.
DefaultIdleTimeout = 180 * time.Second
// DefaultGraceTimeout controls how long Traefik serves pending requests
// prior to shutting down.
DefaultGraceTimeout = 10 * time.Second
// DefaultAcmeCAServer is the default ACME API endpoint
DefaultAcmeCAServer = "https://acme-v02.api.letsencrypt.org/directory"
)
// GlobalConfiguration holds global configuration (with providers, etc.).
// It's populated from the traefik configuration file passed as an argument to the binary.
type GlobalConfiguration struct {
LifeCycle *LifeCycle `description:"Timeouts influencing the server life cycle" export:"true"`
Debug bool `short:"d" description:"Enable debug mode" 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"`
AccessLog *types.AccessLog `description:"Access log settings" export:"true"`
TraefikLog *types.TraefikLog `description:"Traefik log settings" export:"true"`
Tracing *tracing.Tracing `description:"OpenTracing configuration" export:"true"`
LogLevel string `short:"l" description:"Log level" export:"true"`
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key;prod/traefik.crt,prod/traefik.key'" export:"true"`
Cluster *types.Cluster
Constraints types.Constraints `description:"Filter services by constraint, matching with service tags" export:"true"`
ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL" export:"true"`
DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint" export:"true"`
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"`
MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" export:"true"`
InsecureSkipVerify bool `description:"Disable SSL certificate verification" export:"true"`
RootCAs tls.FilesOrContents `description:"Add cert file for self-signed certificate"`
Retry *Retry `description:"Enable retry sending request if network error" export:"true"`
HealthCheck *HealthCheckConfig `description:"Health check parameters" export:"true"`
RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance" export:"true"`
ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers" export:"true"`
KeepTrailingSlash bool `description:"Do not remove trailing slash." export:"true"` // Deprecated
Docker *docker.Provider `description:"Enable Docker backend with default settings" export:"true"`
File *file.Provider `description:"Enable File backend with default settings" export:"true"`
Marathon *marathon.Provider `description:"Enable Marathon backend with default settings" export:"true"`
Consul *consul.Provider `description:"Enable Consul backend with default settings" export:"true"`
ConsulCatalog *consulcatalog.Provider `description:"Enable Consul catalog backend with default settings" export:"true"`
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings" export:"true"`
Zookeeper *zk.Provider `description:"Enable Zookeeper backend with default settings" export:"true"`
Boltdb *boltdb.Provider `description:"Enable Boltdb backend with default settings" export:"true"`
Kubernetes *kubernetes.Provider `description:"Enable Kubernetes backend with default settings" export:"true"`
Mesos *mesos.Provider `description:"Enable Mesos backend with default settings" export:"true"`
Eureka *eureka.Provider `description:"Enable Eureka backend with default settings" export:"true"`
ECS *ecs.Provider `description:"Enable ECS backend with default settings" export:"true"`
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings" export:"true"`
DynamoDB *dynamodb.Provider `description:"Enable DynamoDB backend with default settings" export:"true"`
Rest *rest.Provider `description:"Enable Rest backend with default settings" export:"true"`
API *api.Handler `description:"Enable api/dashboard" export:"true"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"`
Ping *ping.Handler `description:"Enable ping" export:"true"`
HostResolver *HostResolverConfig `description:"Enable CNAME Flattening" export:"true"`
}
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
// It also takes care of maintaining backwards compatibility.
func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
if len(gc.EntryPoints) == 0 {
gc.EntryPoints = map[string]*EntryPoint{"http": {
Address: ":80",
ForwardedHeaders: &ForwardedHeaders{},
}}
gc.DefaultEntryPoints = []string{"http"}
}
if (gc.API != nil && gc.API.EntryPoint == DefaultInternalEntryPointName) ||
(gc.Ping != nil && gc.Ping.EntryPoint == DefaultInternalEntryPointName) ||
(gc.Metrics != nil && gc.Metrics.Prometheus != nil && gc.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) ||
(gc.Rest != nil && gc.Rest.EntryPoint == DefaultInternalEntryPointName) {
if _, ok := gc.EntryPoints[DefaultInternalEntryPointName]; !ok {
gc.EntryPoints[DefaultInternalEntryPointName] = &EntryPoint{Address: ":8080"}
}
}
for entryPointName := range gc.EntryPoints {
entryPoint := gc.EntryPoints[entryPointName]
// ForwardedHeaders must be remove in the next breaking version
if entryPoint.ForwardedHeaders == nil {
entryPoint.ForwardedHeaders = &ForwardedHeaders{}
}
if entryPoint.TLS != nil && entryPoint.TLS.DefaultCertificate == nil && len(entryPoint.TLS.Certificates) > 0 {
log.Infof("No tls.defaultCertificate given for %s: using the first item in tls.certificates as a fallback.", entryPointName)
entryPoint.TLS.DefaultCertificate = &entryPoint.TLS.Certificates[0]
}
}
// Make sure LifeCycle isn't nil to spare nil checks elsewhere.
if gc.LifeCycle == nil {
gc.LifeCycle = &LifeCycle{}
}
if gc.Rancher != nil {
// Ensure backwards compatibility for now
if len(gc.Rancher.AccessKey) > 0 ||
len(gc.Rancher.Endpoint) > 0 ||
len(gc.Rancher.SecretKey) > 0 {
if gc.Rancher.API == nil {
gc.Rancher.API = &rancher.APIConfiguration{
AccessKey: gc.Rancher.AccessKey,
SecretKey: gc.Rancher.SecretKey,
Endpoint: gc.Rancher.Endpoint,
}
}
log.Warn("Deprecated configuration found: rancher.[accesskey|secretkey|endpoint]. " +
"Please use rancher.api.[accesskey|secretkey|endpoint] instead.")
}
if gc.Rancher.Metadata != nil && len(gc.Rancher.Metadata.Prefix) == 0 {
gc.Rancher.Metadata.Prefix = "latest"
}
}
if gc.API != nil {
gc.API.Debug = gc.Debug
}
if gc.File != nil {
gc.File.TraefikFile = configFile
}
gc.initACMEProvider()
gc.initTracing()
}
func (gc *GlobalConfiguration) initTracing() {
if gc.Tracing != nil {
switch gc.Tracing.Backend {
case jaeger.Name:
if gc.Tracing.Jaeger == nil {
gc.Tracing.Jaeger = &jaeger.Config{
SamplingServerURL: "http://localhost:5778/sampling",
SamplingType: "const",
SamplingParam: 1.0,
LocalAgentHostPort: "127.0.0.1:6831",
Propagation: "jaeger",
Gen128Bit: false,
}
}
if gc.Tracing.Zipkin != nil {
log.Warn("Zipkin configuration will be ignored")
gc.Tracing.Zipkin = nil
}
if gc.Tracing.DataDog != nil {
log.Warn("DataDog configuration will be ignored")
gc.Tracing.DataDog = nil
}
case zipkin.Name:
if gc.Tracing.Zipkin == nil {
gc.Tracing.Zipkin = &zipkin.Config{
HTTPEndpoint: "http://localhost:9411/api/v1/spans",
SameSpan: false,
ID128Bit: true,
Debug: false,
SampleRate: 1.0,
}
}
if gc.Tracing.Jaeger != nil {
log.Warn("Jaeger configuration will be ignored")
gc.Tracing.Jaeger = nil
}
if gc.Tracing.DataDog != nil {
log.Warn("DataDog configuration will be ignored")
gc.Tracing.DataDog = nil
}
case datadog.Name:
if gc.Tracing.DataDog == nil {
gc.Tracing.DataDog = &datadog.Config{
LocalAgentHostPort: "localhost:8126",
GlobalTag: "",
Debug: false,
}
}
if gc.Tracing.Zipkin != nil {
log.Warn("Zipkin configuration will be ignored")
gc.Tracing.Zipkin = nil
}
if gc.Tracing.Jaeger != nil {
log.Warn("Jaeger configuration will be ignored")
gc.Tracing.Jaeger = nil
}
default:
log.Warnf("Unknown tracer %q", gc.Tracing.Backend)
return
}
}
}
func (gc *GlobalConfiguration) initACMEProvider() {
if gc.ACME != nil {
gc.ACME.CAServer = getSafeACMECAServer(gc.ACME.CAServer)
if gc.ACME.DNSChallenge != nil && gc.ACME.HTTPChallenge != nil {
log.Warn("Unable to use DNS challenge and HTTP challenge at the same time. Fallback to DNS challenge.")
gc.ACME.HTTPChallenge = nil
}
if gc.ACME.DNSChallenge != nil && gc.ACME.TLSChallenge != nil {
log.Warn("Unable to use DNS challenge and TLS challenge at the same time. Fallback to DNS challenge.")
gc.ACME.TLSChallenge = nil
}
if gc.ACME.HTTPChallenge != nil && gc.ACME.TLSChallenge != nil {
log.Warn("Unable to use HTTP challenge and TLS challenge at the same time. Fallback to TLS challenge.")
gc.ACME.HTTPChallenge = nil
}
if gc.ACME.OnDemand {
log.Warn("ACME.OnDemand is deprecated")
}
}
}
// InitACMEProvider create an acme provider from the ACME part of globalConfiguration
func (gc *GlobalConfiguration) InitACMEProvider() (*acmeprovider.Provider, error) {
if gc.ACME != nil {
if len(gc.ACME.Storage) == 0 {
// Delete the ACME configuration to avoid starting ACME in cluster mode
gc.ACME = nil
return nil, errors.New("unable to initialize ACME provider with no storage location for the certificates")
}
// TODO: Remove when Provider ACME will replace totally ACME
// If provider file, use Provider ACME instead of ACME
if gc.Cluster == nil {
provider := &acmeprovider.Provider{}
provider.Configuration = convertACMEChallenge(gc.ACME)
store := acmeprovider.NewLocalStore(provider.Storage)
provider.Store = store
acme.ConvertToNewFormat(provider.Storage)
gc.ACME = nil
return provider, nil
}
}
return nil, nil
}
func getSafeACMECAServer(caServerSrc string) string {
if len(caServerSrc) == 0 {
return DefaultAcmeCAServer
}
if strings.HasPrefix(caServerSrc, "https://acme-v01.api.letsencrypt.org") {
caServer := strings.Replace(caServerSrc, "v01", "v02", 1)
log.Warnf("The CA server %[1]q refers to a v01 endpoint of the ACME API, please change to %[2]q. Fallback to %[2]q.", caServerSrc, caServer)
return caServer
}
if strings.HasPrefix(caServerSrc, "https://acme-staging.api.letsencrypt.org") {
caServer := strings.Replace(caServerSrc, "https://acme-staging.api.letsencrypt.org", "https://acme-staging-v02.api.letsencrypt.org", 1)
log.Warnf("The CA server %[1]q refers to a v01 endpoint of the ACME API, please change to %[2]q. Fallback to %[2]q.", caServerSrc, caServer)
return caServer
}
return caServerSrc
}
// ValidateConfiguration validate that configuration is coherent
func (gc *GlobalConfiguration) ValidateConfiguration() {
if gc.ACME != nil {
if _, ok := gc.EntryPoints[gc.ACME.EntryPoint]; !ok {
log.Fatalf("Unknown entrypoint %q for ACME configuration", gc.ACME.EntryPoint)
} else {
if gc.EntryPoints[gc.ACME.EntryPoint].TLS == nil {
log.Fatalf("Entrypoint %q has no TLS configuration for ACME configuration", gc.ACME.EntryPoint)
}
}
}
}
// DefaultEntryPoints holds default entry points
type DefaultEntryPoints []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 (dep *DefaultEntryPoints) String() string {
return strings.Join(*dep, ",")
}
// 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 (dep *DefaultEntryPoints) Set(value string) error {
entrypoints := strings.Split(value, ",")
if len(entrypoints) == 0 {
return fmt.Errorf("bad DefaultEntryPoints format: %s", value)
}
for _, entrypoint := range entrypoints {
*dep = append(*dep, entrypoint)
}
return nil
}
// Get return the EntryPoints map
func (dep *DefaultEntryPoints) Get() interface{} {
return *dep
}
// SetValue sets the EntryPoints map with val
func (dep *DefaultEntryPoints) SetValue(val interface{}) {
*dep = val.(DefaultEntryPoints)
}
// Type is type of the struct
func (dep *DefaultEntryPoints) Type() string {
return "defaultentrypoints"
}
// Retry contains request retry config
type Retry struct {
Attempts int `description:"Number of attempts" export:"true"`
}
// HealthCheckConfig contains health check configuration parameters.
type HealthCheckConfig struct {
Interval parse.Duration `description:"Default periodicity of enabled health checks" export:"true"`
Timeout parse.Duration `description:"Default request timeout of enabled health checks" export:"true"`
}
// RespondingTimeouts contains timeout configurations for incoming requests to the Traefik instance.
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"`
WriteTimeout parse.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"`
}
// ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers.
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"`
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"`
}
// LifeCycle contains configurations relevant to the lifecycle (such as the
// shutdown phase) of Traefik.
type LifeCycle struct {
RequestAcceptGraceTimeout parse.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"`
}
// HostResolverConfig contain configuration for CNAME Flattening
type HostResolverConfig struct {
CnameFlattening bool `description:"A flag to enable/disable CNAME flattening" export:"true"`
ResolvConfig string `description:"resolv.conf used for DNS resolving" export:"true"`
ResolvDepth int `description:"The maximal depth of DNS recursive resolving" export:"true"`
}
// Deprecated
func convertACMEChallenge(oldACMEChallenge *acme.ACME) *acmeprovider.Configuration {
conf := &acmeprovider.Configuration{
KeyType: oldACMEChallenge.KeyType,
OnHostRule: oldACMEChallenge.OnHostRule,
OnDemand: oldACMEChallenge.OnDemand,
Email: oldACMEChallenge.Email,
Storage: oldACMEChallenge.Storage,
ACMELogging: oldACMEChallenge.ACMELogging,
CAServer: oldACMEChallenge.CAServer,
EntryPoint: oldACMEChallenge.EntryPoint,
}
for _, domain := range oldACMEChallenge.Domains {
if domain.Main != lego.UnFqdn(domain.Main) {
log.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
}
for _, san := range domain.SANs {
if san != lego.UnFqdn(san) {
log.Warnf("FQDN detected, please remove the trailing dot: %s", san)
}
}
conf.Domains = append(conf.Domains, newtypes.Domain(domain))
}
if oldACMEChallenge.HTTPChallenge != nil {
conf.HTTPChallenge = &acmeprovider.HTTPChallenge{
EntryPoint: oldACMEChallenge.HTTPChallenge.EntryPoint,
}
}
if oldACMEChallenge.DNSChallenge != nil {
conf.DNSChallenge = &acmeprovider.DNSChallenge{
Provider: oldACMEChallenge.DNSChallenge.Provider,
DelayBeforeCheck: oldACMEChallenge.DNSChallenge.DelayBeforeCheck,
}
}
if oldACMEChallenge.TLSChallenge != nil {
conf.TLSChallenge = &acmeprovider.TLSChallenge{}
}
return conf
}

View file

@ -0,0 +1,224 @@
package configuration
import (
"testing"
"github.com/containous/traefik/acme"
"github.com/containous/traefik/old/middlewares/tracing"
"github.com/containous/traefik/old/middlewares/tracing/jaeger"
"github.com/containous/traefik/old/middlewares/tracing/zipkin"
"github.com/containous/traefik/old/provider"
acmeprovider "github.com/containous/traefik/old/provider/acme"
"github.com/containous/traefik/old/provider/file"
"github.com/stretchr/testify/assert"
)
const defaultConfigFile = "traefik.toml"
func TestSetEffectiveConfigurationFileProviderFilename(t *testing.T) {
testCases := []struct {
desc string
fileProvider *file.Provider
wantFileProviderFilename string
wantFileProviderTraefikFile string
}{
{
desc: "no filename for file provider given",
fileProvider: &file.Provider{},
wantFileProviderFilename: "",
wantFileProviderTraefikFile: defaultConfigFile,
},
{
desc: "filename for file provider given",
fileProvider: &file.Provider{BaseProvider: provider.BaseProvider{Filename: "other.toml"}},
wantFileProviderFilename: "other.toml",
wantFileProviderTraefikFile: defaultConfigFile,
},
{
desc: "directory for file provider given",
fileProvider: &file.Provider{Directory: "/"},
wantFileProviderFilename: "",
wantFileProviderTraefikFile: defaultConfigFile,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gc := &GlobalConfiguration{
File: test.fileProvider,
}
gc.SetEffectiveConfiguration(defaultConfigFile)
assert.Equal(t, test.wantFileProviderFilename, gc.File.Filename)
assert.Equal(t, test.wantFileProviderTraefikFile, gc.File.TraefikFile)
})
}
}
func TestSetEffectiveConfigurationTracing(t *testing.T) {
testCases := []struct {
desc string
tracing *tracing.Tracing
expected *tracing.Tracing
}{
{
desc: "no tracing configuration",
tracing: &tracing.Tracing{},
expected: &tracing.Tracing{},
},
{
desc: "tracing bad backend name",
tracing: &tracing.Tracing{
Backend: "powpow",
},
expected: &tracing.Tracing{
Backend: "powpow",
},
},
{
desc: "tracing jaeger backend name",
tracing: &tracing.Tracing{
Backend: "jaeger",
Zipkin: &zipkin.Config{
HTTPEndpoint: "http://localhost:9411/api/v1/spans",
SameSpan: false,
ID128Bit: true,
Debug: false,
},
},
expected: &tracing.Tracing{
Backend: "jaeger",
Jaeger: &jaeger.Config{
SamplingServerURL: "http://localhost:5778/sampling",
SamplingType: "const",
SamplingParam: 1.0,
LocalAgentHostPort: "127.0.0.1:6831",
Propagation: "jaeger",
Gen128Bit: false,
},
Zipkin: nil,
},
},
{
desc: "tracing zipkin backend name",
tracing: &tracing.Tracing{
Backend: "zipkin",
Jaeger: &jaeger.Config{
SamplingServerURL: "http://localhost:5778/sampling",
SamplingType: "const",
SamplingParam: 1.0,
LocalAgentHostPort: "127.0.0.1:6831",
},
},
expected: &tracing.Tracing{
Backend: "zipkin",
Jaeger: nil,
Zipkin: &zipkin.Config{
HTTPEndpoint: "http://localhost:9411/api/v1/spans",
SameSpan: false,
ID128Bit: true,
Debug: false,
SampleRate: 1.0,
},
},
},
{
desc: "tracing zipkin backend name value override",
tracing: &tracing.Tracing{
Backend: "zipkin",
Jaeger: &jaeger.Config{
SamplingServerURL: "http://localhost:5778/sampling",
SamplingType: "const",
SamplingParam: 1.0,
LocalAgentHostPort: "127.0.0.1:6831",
},
Zipkin: &zipkin.Config{
HTTPEndpoint: "http://powpow:9411/api/v1/spans",
SameSpan: true,
ID128Bit: true,
Debug: true,
SampleRate: 0.02,
},
},
expected: &tracing.Tracing{
Backend: "zipkin",
Jaeger: nil,
Zipkin: &zipkin.Config{
HTTPEndpoint: "http://powpow:9411/api/v1/spans",
SameSpan: true,
ID128Bit: true,
Debug: true,
SampleRate: 0.02,
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gc := &GlobalConfiguration{
Tracing: test.tracing,
}
gc.SetEffectiveConfiguration(defaultConfigFile)
assert.Equal(t, test.expected, gc.Tracing)
})
}
}
func TestInitACMEProvider(t *testing.T) {
testCases := []struct {
desc string
acmeConfiguration *acme.ACME
expectedConfiguration *acmeprovider.Provider
noError bool
}{
{
desc: "No ACME configuration",
acmeConfiguration: nil,
expectedConfiguration: nil,
noError: true,
},
{
desc: "ACME configuration with storage",
acmeConfiguration: &acme.ACME{Storage: "foo/acme.json"},
expectedConfiguration: &acmeprovider.Provider{Configuration: &acmeprovider.Configuration{Storage: "foo/acme.json"}},
noError: true,
},
{
desc: "ACME configuration with no storage",
acmeConfiguration: &acme.ACME{},
expectedConfiguration: nil,
noError: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gc := &GlobalConfiguration{
ACME: test.acmeConfiguration,
}
configuration, err := gc.InitACMEProvider()
assert.True(t, (err == nil) == test.noError)
if test.expectedConfiguration == nil {
assert.Nil(t, configuration)
} else {
assert.Equal(t, test.expectedConfiguration.Storage, configuration.Storage)
}
})
}
}

View file

@ -0,0 +1,322 @@
package configuration
import (
"fmt"
"strconv"
"strings"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/tls"
)
// EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...)
type EntryPoint struct {
Address string
TLS *tls.TLS `export:"true"`
Redirect *types.Redirect `export:"true"`
Auth *types.Auth `export:"true"`
WhiteList *types.WhiteList `export:"true"`
Compress *Compress `export:"true"`
ProxyProtocol *ProxyProtocol `export:"true"`
ForwardedHeaders *ForwardedHeaders `export:"true"`
ClientIPStrategy *types.IPStrategy `export:"true"`
}
// Compress contains compress configuration
type Compress struct{}
// ProxyProtocol contains Proxy-Protocol configuration
type ProxyProtocol struct {
Insecure bool `export:"true"`
TrustedIPs []string
}
// ForwardedHeaders Trust client forwarding headers
type ForwardedHeaders struct {
Insecure bool `export:"true"`
TrustedIPs []string
}
// EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...)
type EntryPoints map[string]*EntryPoint
// 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 (ep EntryPoints) String() string {
return fmt.Sprintf("%+v", map[string]*EntryPoint(ep))
}
// 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)
var compress *Compress
if len(result["compress"]) > 0 {
compress = &Compress{}
}
configTLS, err := makeEntryPointTLS(result)
if err != nil {
return err
}
(*ep)[result["name"]] = &EntryPoint{
Address: result["address"],
TLS: configTLS,
Auth: makeEntryPointAuth(result),
Redirect: makeEntryPointRedirect(result),
Compress: compress,
WhiteList: makeWhiteList(result),
ProxyProtocol: makeEntryPointProxyProtocol(result),
ForwardedHeaders: makeEntryPointForwardedHeaders(result),
ClientIPStrategy: makeIPStrategy("clientipstrategy", result),
}
return nil
}
func makeWhiteList(result map[string]string) *types.WhiteList {
if rawRange, ok := result["whitelist_sourcerange"]; ok {
return &types.WhiteList{
SourceRange: strings.Split(rawRange, ","),
IPStrategy: makeIPStrategy("whitelist_ipstrategy", result),
}
}
return nil
}
func makeIPStrategy(prefix string, result map[string]string) *types.IPStrategy {
depth := toInt(result, prefix+"_depth")
excludedIPs := result[prefix+"_excludedips"]
if depth == 0 && len(excludedIPs) == 0 {
return nil
}
return &types.IPStrategy{
Depth: depth,
ExcludedIPs: strings.Split(excludedIPs, ","),
}
}
func makeEntryPointAuth(result map[string]string) *types.Auth {
var basic *types.Basic
if v, ok := result["auth_basic_users"]; ok {
basic = &types.Basic{
Realm: result["auth_basic_realm"],
Users: strings.Split(v, ","),
RemoveHeader: toBool(result, "auth_basic_removeheader"),
}
}
var digest *types.Digest
if v, ok := result["auth_digest_users"]; ok {
digest = &types.Digest{
Users: strings.Split(v, ","),
RemoveHeader: toBool(result, "auth_digest_removeheader"),
}
}
var forward *types.Forward
if address, ok := result["auth_forward_address"]; ok {
var clientTLS *types.ClientTLS
cert := result["auth_forward_tls_cert"]
key := result["auth_forward_tls_key"]
insecureSkipVerify := toBool(result, "auth_forward_tls_insecureskipverify")
if len(cert) > 0 && len(key) > 0 || insecureSkipVerify {
clientTLS = &types.ClientTLS{
CA: result["auth_forward_tls_ca"],
CAOptional: toBool(result, "auth_forward_tls_caoptional"),
Cert: cert,
Key: key,
InsecureSkipVerify: insecureSkipVerify,
}
}
var authResponseHeaders []string
if v, ok := result["auth_forward_authresponseheaders"]; ok {
authResponseHeaders = strings.Split(v, ",")
}
forward = &types.Forward{
Address: address,
TLS: clientTLS,
TrustForwardHeader: toBool(result, "auth_forward_trustforwardheader"),
AuthResponseHeaders: authResponseHeaders,
}
}
var auth *types.Auth
if basic != nil || digest != nil || forward != nil {
auth = &types.Auth{
Basic: basic,
Digest: digest,
Forward: forward,
HeaderField: result["auth_headerfield"],
}
}
return auth
}
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 makeEntryPointForwardedHeaders(result map[string]string) *ForwardedHeaders {
forwardedHeaders := &ForwardedHeaders{}
if _, ok := result["forwardedheaders_insecure"]; ok {
forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure")
}
fhTrustedIPs := result["forwardedheaders_trustedips"]
if len(fhTrustedIPs) > 0 {
// TODO must be removed in the next breaking version.
forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure")
forwardedHeaders.TrustedIPs = strings.Split(fhTrustedIPs, ",")
}
return forwardedHeaders
}
func makeEntryPointRedirect(result map[string]string) *types.Redirect {
var redirect *types.Redirect
if len(result["redirect_entrypoint"]) > 0 || len(result["redirect_regex"]) > 0 || len(result["redirect_replacement"]) > 0 {
redirect = &types.Redirect{
EntryPoint: result["redirect_entrypoint"],
Regex: result["redirect_regex"],
Replacement: result["redirect_replacement"],
Permanent: toBool(result, "redirect_permanent"),
}
}
return redirect
}
func makeEntryPointTLS(result map[string]string) (*tls.TLS, error) {
var configTLS *tls.TLS
if len(result["tls"]) > 0 {
certs := tls.Certificates{}
if err := certs.Set(result["tls"]); err != nil {
return nil, err
}
configTLS = &tls.TLS{
Certificates: certs,
}
} else if len(result["tls_acme"]) > 0 {
configTLS = &tls.TLS{
Certificates: tls.Certificates{},
}
}
if configTLS != nil {
if len(result["ca"]) > 0 {
files := tls.FilesOrContents{}
files.Set(result["ca"])
optional := toBool(result, "ca_optional")
configTLS.ClientCA = tls.ClientCA{
Files: files,
Optional: optional,
}
}
if len(result["tls_minversion"]) > 0 {
configTLS.MinVersion = result["tls_minversion"]
}
if len(result["tls_ciphersuites"]) > 0 {
configTLS.CipherSuites = strings.Split(result["tls_ciphersuites"], ",")
}
if len(result["tls_snistrict"]) > 0 {
configTLS.SniStrict = toBool(result, "tls_snistrict")
}
if len(result["tls_defaultcertificate_cert"]) > 0 && len(result["tls_defaultcertificate_key"]) > 0 {
configTLS.DefaultCertificate = &tls.Certificate{
CertFile: tls.FileOrContent(result["tls_defaultcertificate_cert"]),
KeyFile: tls.FileOrContent(result["tls_defaultcertificate_key"]),
}
}
}
return configTLS, nil
}
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 toInt(conf map[string]string, key string) int {
if val, ok := conf[key]; ok {
intVal, err := strconv.Atoi(val)
if err != nil {
return 0
}
return intVal
}
return 0
}

View file

@ -0,0 +1,515 @@
package configuration
import (
"testing"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/tls"
"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 " +
"TLS:goo,gii " +
"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 " +
"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",
"tls": "goo,gii",
"tls_acme": "TLS",
"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",
"tls_minversion": "VersionTLS11",
"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",
},
},
{
name: "TLS",
value: "Name:foo TLS:goo TLS",
expectedResult: map[string]string{
"name": "foo",
"tls": "goo",
"tls_acme": "TLS",
},
},
}
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 " +
"TLS:goo,gii;foo,fii " +
"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 " +
"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 ",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
Address: ":8000",
TLS: &tls.TLS{
MinVersion: "VersionTLS11",
CipherSuites: []string{"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"},
Certificates: tls.Certificates{
{
CertFile: tls.FileOrContent("goo"),
KeyFile: tls.FileOrContent("gii"),
},
{
CertFile: tls.FileOrContent("foo"),
KeyFile: tls.FileOrContent("fii"),
},
},
ClientCA: tls.ClientCA{
Files: tls.FilesOrContents{"car"},
Optional: true,
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
Regex: "http://localhost/(.*)",
Replacement: "http://mydomain/$1",
Permanent: true,
},
Auth: &types.Auth{
Basic: &types.Basic{
Realm: "myRealm",
RemoveHeader: true,
Users: types.Users{
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
Digest: &types.Digest{
RemoveHeader: true,
Users: types.Users{
"test:traefik:a2688e031edb4be6a3797f3882655c05",
"test2:traefik:518845800f9e2bfb1f1f740ec24f074e",
},
},
Forward: &types.Forward{
Address: "https://authserver.com/auth",
AuthResponseHeaders: []string{"X-Auth", "X-Test", "X-Secret"},
TLS: &types.ClientTLS{
CA: "path/to/local.crt",
CAOptional: true,
Cert: "path/to/foo.cert",
Key: "path/to/foo.key",
InsecureSkipVerify: true,
},
TrustForwardHeader: true,
},
HeaderField: "X-WebAuth-User",
},
WhiteList: &types.WhiteList{
SourceRange: []string{
"10.42.0.0/16",
"152.89.1.33/32",
"afed:be44::/16",
},
IPStrategy: &types.IPStrategy{
Depth: 3,
ExcludedIPs: []string{
"10.0.0.3/24",
"20.0.0.3/24",
},
},
},
Compress: &Compress{},
ProxyProtocol: &ProxyProtocol{
Insecure: false,
TrustedIPs: []string{"192.168.0.1"},
},
ForwardedHeaders: &ForwardedHeaders{
Insecure: false,
TrustedIPs: []string{
"10.0.0.3/24",
"20.0.0.3/24",
},
},
ClientIPStrategy: &types.IPStrategy{
Depth: 3,
ExcludedIPs: []string{
"10.0.0.3/24",
"20.0.0.3/24",
},
},
},
},
{
name: "all parameters lowercase",
expression: "Name:foo " +
"address::8000 " +
"tls:goo,gii;foo,fii " +
"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 " +
"redirect.entryPoint:https " +
"redirect.regex:http://localhost/(.*) " +
"redirect.replacement:http://mydomain/$1 " +
"redirect.permanent:true " +
"compress:true " +
"whiteList.sourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"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.digest.users:test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e " +
"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 ",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
Address: ":8000",
TLS: &tls.TLS{
MinVersion: "VersionTLS11",
CipherSuites: []string{"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"},
Certificates: tls.Certificates{
{
CertFile: tls.FileOrContent("goo"),
KeyFile: tls.FileOrContent("gii"),
},
{
CertFile: tls.FileOrContent("foo"),
KeyFile: tls.FileOrContent("fii"),
},
},
ClientCA: tls.ClientCA{
Files: tls.FilesOrContents{"car"},
Optional: true,
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
Regex: "http://localhost/(.*)",
Replacement: "http://mydomain/$1",
Permanent: true,
},
Auth: &types.Auth{
Basic: &types.Basic{
Realm: "myRealm",
Users: types.Users{
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
Digest: &types.Digest{
Users: types.Users{
"test:traefik:a2688e031edb4be6a3797f3882655c05",
"test2:traefik:518845800f9e2bfb1f1f740ec24f074e",
},
},
Forward: &types.Forward{
Address: "https://authserver.com/auth",
AuthResponseHeaders: []string{"X-Auth", "X-Test", "X-Secret"},
TLS: &types.ClientTLS{
CA: "path/to/local.crt",
CAOptional: true,
Cert: "path/to/foo.cert",
Key: "path/to/foo.key",
InsecureSkipVerify: true,
},
TrustForwardHeader: true,
},
HeaderField: "X-WebAuth-User",
},
WhiteList: &types.WhiteList{
SourceRange: []string{
"10.42.0.0/16",
"152.89.1.33/32",
"afed:be44::/16",
},
},
Compress: &Compress{},
ProxyProtocol: &ProxyProtocol{
Insecure: false,
TrustedIPs: []string{"192.168.0.1"},
},
ForwardedHeaders: &ForwardedHeaders{
Insecure: false,
TrustedIPs: []string{
"10.0.0.3/24",
"20.0.0.3/24",
},
},
},
},
{
name: "default",
expression: "Name:foo",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{Insecure: false},
},
},
{
name: "ForwardedHeaders insecure true",
expression: "Name:foo ForwardedHeaders.insecure:true",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
},
},
{
name: "ForwardedHeaders insecure false",
expression: "Name:foo ForwardedHeaders.insecure:false",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{Insecure: false},
},
},
{
name: "ForwardedHeaders TrustedIPs",
expression: "Name:foo ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{
TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"},
},
},
},
{
name: "ProxyProtocol insecure true",
expression: "Name:foo ProxyProtocol.insecure:true",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{},
ProxyProtocol: &ProxyProtocol{Insecure: true},
},
},
{
name: "ProxyProtocol insecure false",
expression: "Name:foo ProxyProtocol.insecure:false",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{},
ProxyProtocol: &ProxyProtocol{},
},
},
{
name: "ProxyProtocol TrustedIPs",
expression: "Name:foo ProxyProtocol.TrustedIPs:10.0.0.3/24,20.0.0.3/24",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
ForwardedHeaders: &ForwardedHeaders{},
ProxyProtocol: &ProxyProtocol{
TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"},
},
},
},
{
name: "compress on",
expression: "Name:foo Compress:on",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
Compress: &Compress{},
ForwardedHeaders: &ForwardedHeaders{},
},
},
{
name: "compress true",
expression: "Name:foo Compress:true",
expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{
Compress: &Compress{},
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

@ -0,0 +1,111 @@
package configuration
import (
"encoding/json"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/provider"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
)
// ProviderAggregator aggregate providers
type ProviderAggregator struct {
providers []provider.Provider
constraints types.Constraints
}
// NewProviderAggregator return an aggregate of all the providers configured in GlobalConfiguration
func NewProviderAggregator(gc *GlobalConfiguration) ProviderAggregator {
provider := ProviderAggregator{
constraints: gc.Constraints,
}
if gc.Docker != nil {
provider.quietAddProvider(gc.Docker)
}
if gc.Marathon != nil {
provider.quietAddProvider(gc.Marathon)
}
if gc.File != nil {
provider.quietAddProvider(gc.File)
}
if gc.Rest != nil {
provider.quietAddProvider(gc.Rest)
}
if gc.Consul != nil {
provider.quietAddProvider(gc.Consul)
}
if gc.ConsulCatalog != nil {
provider.quietAddProvider(gc.ConsulCatalog)
}
if gc.Etcd != nil {
provider.quietAddProvider(gc.Etcd)
}
if gc.Zookeeper != nil {
provider.quietAddProvider(gc.Zookeeper)
}
if gc.Boltdb != nil {
provider.quietAddProvider(gc.Boltdb)
}
if gc.Kubernetes != nil {
provider.quietAddProvider(gc.Kubernetes)
}
if gc.Mesos != nil {
provider.quietAddProvider(gc.Mesos)
}
if gc.Eureka != nil {
provider.quietAddProvider(gc.Eureka)
}
if gc.ECS != nil {
provider.quietAddProvider(gc.ECS)
}
if gc.Rancher != nil {
provider.quietAddProvider(gc.Rancher)
}
if gc.DynamoDB != nil {
provider.quietAddProvider(gc.DynamoDB)
}
return provider
}
func (p *ProviderAggregator) quietAddProvider(provider provider.Provider) {
err := p.AddProvider(provider)
if err != nil {
log.Errorf("Error initializing provider %T: %v", provider, err)
}
}
// AddProvider add a provider in the providers map
func (p *ProviderAggregator) AddProvider(provider provider.Provider) error {
err := provider.Init(p.constraints)
if err != nil {
return err
}
p.providers = append(p.providers, provider)
return nil
}
// Init the provider
func (p ProviderAggregator) Init(_ types.Constraints) error {
return nil
}
// Provide call the provide method of every providers
func (p ProviderAggregator) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
for _, p := range p.providers {
jsonConf, err := json.Marshal(p)
if err != nil {
log.Debugf("Unable to marshal provider conf %T with error: %v", p, err)
}
log.Infof("Starting provider %T %s", p, jsonConf)
currentProvider := p
safe.Go(func() {
err := currentProvider.Provide(configurationChan, pool)
if err != nil {
log.Errorf("Error starting provider %T: %v", p, err)
}
})
}
return nil
}

View file

@ -0,0 +1,116 @@
package router
import (
"github.com/containous/mux"
"github.com/containous/traefik/old/configuration"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/middlewares"
mauth "github.com/containous/traefik/old/middlewares/auth"
"github.com/containous/traefik/old/types"
"github.com/urfave/negroni"
)
// NewInternalRouterAggregator Create a new internalRouterAggregator
func NewInternalRouterAggregator(globalConfiguration configuration.GlobalConfiguration, entryPointName string) *InternalRouterAggregator {
var serverMiddlewares []negroni.Handler
if globalConfiguration.EntryPoints[entryPointName].WhiteList != nil {
ipStrategy := globalConfiguration.EntryPoints[entryPointName].ClientIPStrategy
if globalConfiguration.EntryPoints[entryPointName].WhiteList.IPStrategy != nil {
ipStrategy = globalConfiguration.EntryPoints[entryPointName].WhiteList.IPStrategy
}
strategy, err := ipStrategy.Get()
if err != nil {
log.Fatalf("Error creating whitelist middleware: %s", err)
}
ipWhitelistMiddleware, err := middlewares.NewIPWhiteLister(globalConfiguration.EntryPoints[entryPointName].WhiteList.SourceRange, strategy)
if err != nil {
log.Fatalf("Error creating whitelist middleware: %s", err)
}
if ipWhitelistMiddleware != nil {
serverMiddlewares = append(serverMiddlewares, ipWhitelistMiddleware)
}
}
if globalConfiguration.EntryPoints[entryPointName].Auth != nil {
authMiddleware, err := mauth.NewAuthenticator(globalConfiguration.EntryPoints[entryPointName].Auth, nil)
if err != nil {
log.Fatalf("Error creating authenticator middleware: %s", err)
}
serverMiddlewares = append(serverMiddlewares, authMiddleware)
}
router := InternalRouterAggregator{}
routerWithMiddleware := InternalRouterAggregator{}
if globalConfiguration.Metrics != nil && globalConfiguration.Metrics.Prometheus != nil && globalConfiguration.Metrics.Prometheus.EntryPoint == entryPointName {
// routerWithMiddleware.AddRouter(metrics.PrometheusHandler{})
}
if globalConfiguration.Rest != nil && globalConfiguration.Rest.EntryPoint == entryPointName {
routerWithMiddleware.AddRouter(globalConfiguration.Rest)
}
if globalConfiguration.API != nil && globalConfiguration.API.EntryPoint == entryPointName {
routerWithMiddleware.AddRouter(globalConfiguration.API)
}
if globalConfiguration.Ping != nil && globalConfiguration.Ping.EntryPoint == entryPointName {
router.AddRouter(globalConfiguration.Ping)
}
if globalConfiguration.ACME != nil && globalConfiguration.ACME.HTTPChallenge != nil && globalConfiguration.ACME.HTTPChallenge.EntryPoint == entryPointName {
router.AddRouter(globalConfiguration.ACME)
}
router.AddRouter(&WithMiddleware{router: &routerWithMiddleware, routerMiddlewares: serverMiddlewares})
return &router
}
// WithMiddleware router with internal middleware
type WithMiddleware struct {
router types.InternalRouter
routerMiddlewares []negroni.Handler
}
// AddRoutes Add routes to the router
func (wm *WithMiddleware) AddRoutes(systemRouter *mux.Router) {
realRouter := systemRouter.PathPrefix("/").Subrouter()
wm.router.AddRoutes(realRouter)
if len(wm.routerMiddlewares) > 0 {
if err := realRouter.Walk(wrapRoute(wm.routerMiddlewares)); err != nil {
log.Error(err)
}
}
}
// InternalRouterAggregator InternalRouter that aggregate other internalRouter
type InternalRouterAggregator struct {
internalRouters []types.InternalRouter
}
// AddRouter add a router in the aggregator
func (r *InternalRouterAggregator) AddRouter(router types.InternalRouter) {
r.internalRouters = append(r.internalRouters, router)
}
// AddRoutes Add routes to the router
func (r *InternalRouterAggregator) AddRoutes(systemRouter *mux.Router) {
for _, router := range r.internalRouters {
router.AddRoutes(systemRouter)
}
}
// wrapRoute with middlewares
func wrapRoute(middlewares []negroni.Handler) func(*mux.Route, *mux.Router, []*mux.Route) error {
return func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
middles := append(middlewares, negroni.Wrap(route.GetHandler()))
route.Handler(negroni.New(middles...))
return nil
}
}

View file

@ -0,0 +1,151 @@
package router
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/mux"
"github.com/containous/traefik/acme"
"github.com/containous/traefik/old/api"
"github.com/containous/traefik/old/configuration"
"github.com/containous/traefik/old/log"
"github.com/containous/traefik/old/ping"
acmeprovider "github.com/containous/traefik/old/provider/acme"
"github.com/containous/traefik/old/types"
"github.com/containous/traefik/safe"
"github.com/stretchr/testify/assert"
"github.com/urfave/negroni"
)
func TestNewInternalRouterAggregatorWithAuth(t *testing.T) {
currentConfiguration := &safe.Safe{}
currentConfiguration.Set(types.Configurations{})
globalConfiguration := configuration.GlobalConfiguration{
API: &api.Handler{
EntryPoint: "traefik",
CurrentConfigurations: currentConfiguration,
},
Ping: &ping.Handler{
EntryPoint: "traefik",
},
ACME: &acme.ACME{
HTTPChallenge: &acmeprovider.HTTPChallenge{
EntryPoint: "traefik",
},
},
EntryPoints: configuration.EntryPoints{
"traefik": &configuration.EntryPoint{
Auth: &types.Auth{
Basic: &types.Basic{
Users: types.Users{"test:test"},
},
},
},
},
}
testCases := []struct {
desc string
testedURL string
expectedStatusCode int
}{
{
desc: "Wrong url",
testedURL: "/wrong",
expectedStatusCode: 502,
},
{
desc: "Ping without auth",
testedURL: "/ping",
expectedStatusCode: 200,
},
{
desc: "acme without auth",
testedURL: "/.well-known/acme-challenge/token",
expectedStatusCode: 404,
},
{
desc: "api with auth",
testedURL: "/api",
expectedStatusCode: 401,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
router := NewInternalRouterAggregator(globalConfiguration, "traefik")
internalMuxRouter := mux.NewRouter()
router.AddRoutes(internalMuxRouter)
internalMuxRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, test.testedURL, nil)
internalMuxRouter.ServeHTTP(recorder, request)
assert.Equal(t, test.expectedStatusCode, recorder.Code)
})
}
}
type MockInternalRouterFunc func(systemRouter *mux.Router)
func (m MockInternalRouterFunc) AddRoutes(systemRouter *mux.Router) {
m(systemRouter)
}
func TestWithMiddleware(t *testing.T) {
router := WithMiddleware{
router: MockInternalRouterFunc(func(systemRouter *mux.Router) {
systemRouter.Handle("/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte("router")); err != nil {
log.Error(err)
}
}))
}),
routerMiddlewares: []negroni.Handler{
negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if _, err := rw.Write([]byte("before middleware1|")); err != nil {
log.Error(err)
}
next.ServeHTTP(rw, r)
if _, err := rw.Write([]byte("|after middleware1")); err != nil {
log.Error(err)
}
}),
negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if _, err := rw.Write([]byte("before middleware2|")); err != nil {
log.Error(err)
}
next.ServeHTTP(rw, r)
if _, err := rw.Write([]byte("|after middleware2")); err != nil {
log.Error(err)
}
}),
},
}
internalMuxRouter := mux.NewRouter()
router.AddRoutes(internalMuxRouter)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/test", nil)
internalMuxRouter.ServeHTTP(recorder, request)
obtained := recorder.Body.String()
assert.Equal(t, "before middleware1|before middleware2|router|after middleware2|after middleware1", obtained)
}