Add internal provider
Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
This commit is contained in:
parent
2ee2e29262
commit
424e2a9439
71 changed files with 2523 additions and 1469 deletions
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -20,12 +19,17 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
"github.com/containous/traefik/v2/pkg/metrics"
|
||||||
|
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/acme"
|
"github.com/containous/traefik/v2/pkg/provider/acme"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/aggregator"
|
"github.com/containous/traefik/v2/pkg/provider/aggregator"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider/traefik"
|
||||||
"github.com/containous/traefik/v2/pkg/safe"
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
"github.com/containous/traefik/v2/pkg/server"
|
"github.com/containous/traefik/v2/pkg/server"
|
||||||
"github.com/containous/traefik/v2/pkg/server/router"
|
"github.com/containous/traefik/v2/pkg/server/middleware"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/service"
|
||||||
traefiktls "github.com/containous/traefik/v2/pkg/tls"
|
traefiktls "github.com/containous/traefik/v2/pkg/tls"
|
||||||
|
"github.com/containous/traefik/v2/pkg/types"
|
||||||
"github.com/containous/traefik/v2/pkg/version"
|
"github.com/containous/traefik/v2/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"
|
||||||
|
@ -105,42 +109,10 @@ func runCmd(staticConfiguration *static.Configuration) error {
|
||||||
|
|
||||||
stats(staticConfiguration)
|
stats(staticConfiguration)
|
||||||
|
|
||||||
providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers)
|
svr, err := setupServer(staticConfiguration)
|
||||||
|
|
||||||
tlsManager := traefiktls.NewManager()
|
|
||||||
|
|
||||||
acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager)
|
|
||||||
|
|
||||||
serverEntryPointsTCP := make(server.TCPEntryPoints)
|
|
||||||
for entryPointName, config := range staticConfiguration.EntryPoints {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
|
||||||
serverEntryPointsTCP[entryPointName], err = server.NewTCPEntryPoint(ctx, config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err)
|
return err
|
||||||
}
|
}
|
||||||
serverEntryPointsTCP[entryPointName].RouteAppenderFactory = router.NewRouteAppenderFactory(*staticConfiguration, entryPointName, acmeProviders)
|
|
||||||
}
|
|
||||||
|
|
||||||
svr := server.NewServer(*staticConfiguration, providerAggregator, serverEntryPointsTCP, tlsManager)
|
|
||||||
|
|
||||||
resolverNames := map[string]struct{}{}
|
|
||||||
|
|
||||||
for _, p := range acmeProviders {
|
|
||||||
resolverNames[p.ResolverName] = struct{}{}
|
|
||||||
svr.AddListener(p.ListenConfiguration)
|
|
||||||
}
|
|
||||||
|
|
||||||
svr.AddListener(func(config dynamic.Configuration) {
|
|
||||||
for rtName, rt := range config.HTTP.Routers {
|
|
||||||
if rt.TLS == nil || rt.TLS.CertResolver == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
|
|
||||||
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := cmd.ContextWithSignal(context.Background())
|
ctx := cmd.ContextWithSignal(context.Background())
|
||||||
|
|
||||||
|
@ -168,7 +140,7 @@ func runCmd(staticConfiguration *static.Configuration) error {
|
||||||
for range tick {
|
for range tick {
|
||||||
resp, errHealthCheck := healthcheck.Do(*staticConfiguration)
|
resp, errHealthCheck := healthcheck.Do(*staticConfiguration)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if staticConfiguration.Ping == nil || errHealthCheck == nil {
|
if staticConfiguration.Ping == nil || errHealthCheck == nil {
|
||||||
|
@ -188,6 +160,94 @@ func runCmd(staticConfiguration *static.Configuration) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupServer(staticConfiguration *static.Configuration) (*server.Server, error) {
|
||||||
|
providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers)
|
||||||
|
|
||||||
|
// adds internal provider
|
||||||
|
err := providerAggregator.AddProvider(traefik.New(*staticConfiguration))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsManager := traefiktls.NewManager()
|
||||||
|
|
||||||
|
acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager)
|
||||||
|
|
||||||
|
serverEntryPointsTCP, err := server.NewTCPEntryPoints(*staticConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
routinesPool := safe.NewPool(ctx)
|
||||||
|
|
||||||
|
metricsRegistry := registerMetricClients(staticConfiguration.Metrics)
|
||||||
|
accessLog := setupAccessLog(staticConfiguration.AccessLog)
|
||||||
|
chainBuilder := middleware.NewChainBuilder(*staticConfiguration, metricsRegistry, accessLog)
|
||||||
|
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry)
|
||||||
|
tcpRouterFactory := server.NewTCPRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder)
|
||||||
|
|
||||||
|
watcher := server.NewConfigurationWatcher(routinesPool, providerAggregator, time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration))
|
||||||
|
|
||||||
|
watcher.AddListener(func(conf dynamic.Configuration) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates)
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.AddListener(func(_ dynamic.Configuration) {
|
||||||
|
metricsRegistry.ConfigReloadsCounter().Add(1)
|
||||||
|
metricsRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix()))
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.AddListener(switchRouter(tcpRouterFactory, acmeProviders, serverEntryPointsTCP))
|
||||||
|
|
||||||
|
watcher.AddListener(func(conf dynamic.Configuration) {
|
||||||
|
if metricsRegistry.IsEpEnabled() || metricsRegistry.IsSvcEnabled() {
|
||||||
|
var eps []string
|
||||||
|
for key := range serverEntryPointsTCP {
|
||||||
|
eps = append(eps, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.OnConfigurationUpdate(conf, eps)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resolverNames := map[string]struct{}{}
|
||||||
|
for _, p := range acmeProviders {
|
||||||
|
resolverNames[p.ResolverName] = struct{}{}
|
||||||
|
watcher.AddListener(p.ListenConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher.AddListener(func(config dynamic.Configuration) {
|
||||||
|
for rtName, rt := range config.HTTP.Routers {
|
||||||
|
if rt.TLS == nil || rt.TLS.CertResolver == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
|
||||||
|
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return server.NewServer(routinesPool, serverEntryPointsTCP, watcher, chainBuilder, accessLog), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func switchRouter(tcpRouterFactory *server.TCPRouterFactory, acmeProviders []*acme.Provider, serverEntryPointsTCP server.TCPEntryPoints) func(conf dynamic.Configuration) {
|
||||||
|
return func(conf dynamic.Configuration) {
|
||||||
|
routers := tcpRouterFactory.CreateTCPRouters(conf)
|
||||||
|
for entryPointName, rt := range routers {
|
||||||
|
for _, p := range acmeProviders {
|
||||||
|
if p != nil && p.HTTPChallenge != nil && p.HTTPChallenge.EntryPoint == entryPointName {
|
||||||
|
rt.HTTPHandler(p.CreateHandler(rt.GetHTTPHandler()))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverEntryPointsTCP.Switch(routers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// initACMEProvider creates an acme provider from the ACME part of globalConfiguration
|
// initACMEProvider creates an acme provider from the ACME part of globalConfiguration
|
||||||
func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider {
|
func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider {
|
||||||
challengeStore := acme.NewLocalChallengeStore()
|
challengeStore := acme.NewLocalChallengeStore()
|
||||||
|
@ -222,6 +282,60 @@ func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.Pr
|
||||||
return resolvers
|
return resolvers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerMetricClients(metricsConfig *types.Metrics) metrics.Registry {
|
||||||
|
if metricsConfig == nil {
|
||||||
|
return metrics.NewVoidRegistry()
|
||||||
|
}
|
||||||
|
|
||||||
|
var registries []metrics.Registry
|
||||||
|
|
||||||
|
if metricsConfig.Prometheus != nil {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "prometheus"))
|
||||||
|
prometheusRegister := metrics.RegisterPrometheus(ctx, metricsConfig.Prometheus)
|
||||||
|
if prometheusRegister != nil {
|
||||||
|
registries = append(registries, prometheusRegister)
|
||||||
|
log.FromContext(ctx).Debug("Configured Prometheus metrics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if metricsConfig.Datadog != nil {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "datadog"))
|
||||||
|
registries = append(registries, metrics.RegisterDatadog(ctx, metricsConfig.Datadog))
|
||||||
|
log.FromContext(ctx).Debugf("Configured Datadog metrics: pushing to %s once every %s",
|
||||||
|
metricsConfig.Datadog.Address, metricsConfig.Datadog.PushInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if metricsConfig.StatsD != nil {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "statsd"))
|
||||||
|
registries = append(registries, metrics.RegisterStatsd(ctx, metricsConfig.StatsD))
|
||||||
|
log.FromContext(ctx).Debugf("Configured StatsD metrics: pushing to %s once every %s",
|
||||||
|
metricsConfig.StatsD.Address, metricsConfig.StatsD.PushInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if metricsConfig.InfluxDB != nil {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "influxdb"))
|
||||||
|
registries = append(registries, metrics.RegisterInfluxDB(ctx, metricsConfig.InfluxDB))
|
||||||
|
log.FromContext(ctx).Debugf("Configured InfluxDB metrics: pushing to %s once every %s",
|
||||||
|
metricsConfig.InfluxDB.Address, metricsConfig.InfluxDB.PushInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics.NewMultiRegistry(registries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupAccessLog(conf *types.AccessLog) *accesslog.Handler {
|
||||||
|
if conf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accessLoggerMiddleware, err := accesslog.NewHandler(conf)
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Warnf("Unable to create access logger : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessLoggerMiddleware
|
||||||
|
}
|
||||||
|
|
||||||
func configureLogging(staticConfiguration *static.Configuration) {
|
func configureLogging(staticConfiguration *static.Configuration) {
|
||||||
// configure default log flags
|
// configure default log flags
|
||||||
stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
|
stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
|
||||||
|
|
|
@ -116,3 +116,25 @@ metrics:
|
||||||
--entryPoints.metrics.address=":8082"
|
--entryPoints.metrics.address=":8082"
|
||||||
--metrics.prometheus.entryPoint="metrics"
|
--metrics.prometheus.entryPoint="metrics"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `manualRouting`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
If `manualRouting` is `true`, it disables the default internal router in order to allow one to create a custom router for the `prometheus@internal` service.
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[metrics]
|
||||||
|
[metrics.prometheus]
|
||||||
|
manualRouting = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
metrics:
|
||||||
|
prometheus:
|
||||||
|
manualRouting: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--metrics.prometheus.manualrouting=true
|
||||||
|
```
|
||||||
|
|
|
@ -58,3 +58,23 @@ ping:
|
||||||
--entryPoints.ping.address=":8082"
|
--entryPoints.ping.address=":8082"
|
||||||
--ping.entryPoint="ping"
|
--ping.entryPoint="ping"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `manualRouting`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
If `manualRouting` is `true`, it disables the default internal router in order to allow one to create a custom router for the `ping@internal` service.
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[ping]
|
||||||
|
manualRouting = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
ping:
|
||||||
|
manualRouting: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--ping.manualrouting=true
|
||||||
|
```
|
||||||
|
|
|
@ -213,6 +213,9 @@ Buckets for latency metrics. (Default: ```0.100000, 0.300000, 1.200000, 5.000000
|
||||||
`--metrics.prometheus.entrypoint`:
|
`--metrics.prometheus.entrypoint`:
|
||||||
EntryPoint (Default: ```traefik```)
|
EntryPoint (Default: ```traefik```)
|
||||||
|
|
||||||
|
`--metrics.prometheus.manualrouting`:
|
||||||
|
Manual routing (Default: ```false```)
|
||||||
|
|
||||||
`--metrics.statsd`:
|
`--metrics.statsd`:
|
||||||
StatsD metrics exporter type. (Default: ```false```)
|
StatsD metrics exporter type. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -237,6 +240,9 @@ Enable ping. (Default: ```false```)
|
||||||
`--ping.entrypoint`:
|
`--ping.entrypoint`:
|
||||||
EntryPoint (Default: ```traefik```)
|
EntryPoint (Default: ```traefik```)
|
||||||
|
|
||||||
|
`--ping.manualrouting`:
|
||||||
|
Manual routing (Default: ```false```)
|
||||||
|
|
||||||
`--providers.consulcatalog.cache`:
|
`--providers.consulcatalog.cache`:
|
||||||
Use local agent caching for catalog reads. (Default: ```false```)
|
Use local agent caching for catalog reads. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,9 @@ Buckets for latency metrics. (Default: ```0.100000, 0.300000, 1.200000, 5.000000
|
||||||
`TRAEFIK_METRICS_PROMETHEUS_ENTRYPOINT`:
|
`TRAEFIK_METRICS_PROMETHEUS_ENTRYPOINT`:
|
||||||
EntryPoint (Default: ```traefik```)
|
EntryPoint (Default: ```traefik```)
|
||||||
|
|
||||||
|
`TRAEFIK_METRICS_PROMETHEUS_MANUALROUTING`:
|
||||||
|
Manual routing (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_METRICS_STATSD`:
|
`TRAEFIK_METRICS_STATSD`:
|
||||||
StatsD metrics exporter type. (Default: ```false```)
|
StatsD metrics exporter type. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -237,6 +240,9 @@ Enable ping. (Default: ```false```)
|
||||||
`TRAEFIK_PING_ENTRYPOINT`:
|
`TRAEFIK_PING_ENTRYPOINT`:
|
||||||
EntryPoint (Default: ```traefik```)
|
EntryPoint (Default: ```traefik```)
|
||||||
|
|
||||||
|
`TRAEFIK_PING_MANUALROUTING`:
|
||||||
|
Manual routing (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_CONSULCATALOG_CACHE`:
|
`TRAEFIK_PROVIDERS_CONSULCATALOG_CACHE`:
|
||||||
Use local agent caching for catalog reads. (Default: ```false```)
|
Use local agent caching for catalog reads. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,7 @@
|
||||||
addEntryPointsLabels = true
|
addEntryPointsLabels = true
|
||||||
addServicesLabels = true
|
addServicesLabels = true
|
||||||
entryPoint = "foobar"
|
entryPoint = "foobar"
|
||||||
|
manualRouting = true
|
||||||
[metrics.datadog]
|
[metrics.datadog]
|
||||||
address = "foobar"
|
address = "foobar"
|
||||||
pushInterval = "10s"
|
pushInterval = "10s"
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
|
|
||||||
[ping]
|
[ping]
|
||||||
entryPoint = "foobar"
|
entryPoint = "foobar"
|
||||||
|
manualRouting = true
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "foobar"
|
level = "foobar"
|
||||||
|
|
|
@ -148,6 +148,7 @@ metrics:
|
||||||
addEntryPointsLabels: true
|
addEntryPointsLabels: true
|
||||||
addServicesLabels: true
|
addServicesLabels: true
|
||||||
entryPoint: foobar
|
entryPoint: foobar
|
||||||
|
manualRouting: true
|
||||||
datadog:
|
datadog:
|
||||||
address: foobar
|
address: foobar
|
||||||
pushInterval: 42
|
pushInterval: 42
|
||||||
|
@ -171,6 +172,7 @@ metrics:
|
||||||
addServicesLabels: true
|
addServicesLabels: true
|
||||||
ping:
|
ping:
|
||||||
entryPoint: foobar
|
entryPoint: foobar
|
||||||
|
manualRouting: true
|
||||||
log:
|
log:
|
||||||
level: foobar
|
level: foobar
|
||||||
filePath: foobar
|
filePath: foobar
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -546,8 +547,16 @@ func checkAccessLogExactValuesOutput(c *check.C, values []accessLogValue) int {
|
||||||
func extractLines(c *check.C) []string {
|
func extractLines(c *check.C) []string {
|
||||||
accessLog, err := ioutil.ReadFile(traefikTestAccessLogFile)
|
accessLog, err := ioutil.ReadFile(traefikTestAccessLogFile)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
lines := strings.Split(string(accessLog), "\n")
|
lines := strings.Split(string(accessLog), "\n")
|
||||||
return lines
|
|
||||||
|
var clean []string
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.Contains(line, "/api/rawdata") {
|
||||||
|
clean = append(clean, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clean
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkStatsForLogFile(c *check.C) {
|
func checkStatsForLogFile(c *check.C) {
|
||||||
|
@ -580,28 +589,31 @@ func CheckAccessLogFormat(c *check.C, line string, i int) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(results, checker.HasLen, 14)
|
c.Assert(results, checker.HasLen, 14)
|
||||||
c.Assert(results[accesslog.OriginStatus], checker.Matches, `^(-|\d{3})$`)
|
c.Assert(results[accesslog.OriginStatus], checker.Matches, `^(-|\d{3})$`)
|
||||||
c.Assert(results[accesslog.RequestCount], checker.Equals, fmt.Sprintf("%d", i+1))
|
count, _ := strconv.Atoi(results[accesslog.RequestCount])
|
||||||
c.Assert(results[accesslog.RouterName], checker.Matches, `"rt-.+@docker"`)
|
c.Assert(count, checker.GreaterOrEqualThan, i+1)
|
||||||
c.Assert(results[accesslog.ServiceURL], checker.HasPrefix, "\"http://")
|
c.Assert(results[accesslog.RouterName], checker.Matches, `"(rt-.+@docker|api@internal)"`)
|
||||||
|
c.Assert(results[accesslog.ServiceURL], checker.HasPrefix, `"http://`)
|
||||||
c.Assert(results[accesslog.Duration], checker.Matches, `^\d+ms$`)
|
c.Assert(results[accesslog.Duration], checker.Matches, `^\d+ms$`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAccessLogExactValues(c *check.C, line string, i int, v accessLogValue) {
|
func checkAccessLogExactValues(c *check.C, line string, i int, v accessLogValue) {
|
||||||
results, err := accesslog.ParseAccessLog(line)
|
results, err := accesslog.ParseAccessLog(line)
|
||||||
// c.Assert(nil, checker.Equals, line)
|
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(results, checker.HasLen, 14)
|
c.Assert(results, checker.HasLen, 14)
|
||||||
if len(v.user) > 0 {
|
if len(v.user) > 0 {
|
||||||
c.Assert(results[accesslog.ClientUsername], checker.Equals, v.user)
|
c.Assert(results[accesslog.ClientUsername], checker.Equals, v.user)
|
||||||
}
|
}
|
||||||
c.Assert(results[accesslog.OriginStatus], checker.Equals, v.code)
|
c.Assert(results[accesslog.OriginStatus], checker.Equals, v.code)
|
||||||
c.Assert(results[accesslog.RequestCount], checker.Equals, fmt.Sprintf("%d", i+1))
|
count, _ := strconv.Atoi(results[accesslog.RequestCount])
|
||||||
|
c.Assert(count, checker.GreaterOrEqualThan, i+1)
|
||||||
c.Assert(results[accesslog.RouterName], checker.Matches, `^"?`+v.routerName+`.*(@docker)?$`)
|
c.Assert(results[accesslog.RouterName], checker.Matches, `^"?`+v.routerName+`.*(@docker)?$`)
|
||||||
c.Assert(results[accesslog.ServiceURL], checker.Matches, `^"?`+v.serviceURL+`.*$`)
|
c.Assert(results[accesslog.ServiceURL], checker.Matches, `^"?`+v.serviceURL+`.*$`)
|
||||||
c.Assert(results[accesslog.Duration], checker.Matches, `^\d+ms$`)
|
c.Assert(results[accesslog.Duration], checker.Matches, `^\d+ms$`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForTraefik(c *check.C, containerName string) {
|
func waitForTraefik(c *check.C, containerName string) {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
// Wait for Traefik to turn ready.
|
// Wait for Traefik to turn ready.
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/rawdata", nil)
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/rawdata", nil)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/integration/try"
|
"github.com/containous/traefik/v2/integration/try"
|
||||||
|
@ -70,15 +71,18 @@ func (s *DockerComposeSuite) TestComposeScale(c *check.C) {
|
||||||
err = json.NewDecoder(resp.Body).Decode(&rtconf)
|
err = json.NewDecoder(resp.Body).Decode(&rtconf)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// check that we have only one router
|
// check that we have only three routers (the one from this test + 2 unrelated internal ones)
|
||||||
c.Assert(rtconf.Routers, checker.HasLen, 1)
|
c.Assert(rtconf.Routers, checker.HasLen, 3)
|
||||||
|
|
||||||
// check that we have only one service with n servers
|
// check that we have only one service (not counting the internal ones) with n servers
|
||||||
services := rtconf.Services
|
services := rtconf.Services
|
||||||
c.Assert(services, checker.HasLen, 1)
|
c.Assert(services, checker.HasLen, 3)
|
||||||
for k, v := range services {
|
for name, service := range services {
|
||||||
c.Assert(k, checker.Equals, composeService+"-integrationtest"+composeProject+"@docker")
|
if strings.HasSuffix(name, "@internal") {
|
||||||
c.Assert(v.LoadBalancer.Servers, checker.HasLen, serviceCount)
|
continue
|
||||||
|
}
|
||||||
|
c.Assert(name, checker.Equals, composeService+"-integrationtest"+composeProject+"@docker")
|
||||||
|
c.Assert(service.LoadBalancer.Servers, checker.HasLen, serviceCount)
|
||||||
// We could break here, but we don't just to keep us honest.
|
// We could break here, but we don't just to keep us honest.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -155,11 +156,15 @@ func verifyLogLines(c *check.C, fileName string, countInit int, accessLog bool)
|
||||||
line := rotatedLog.Text()
|
line := rotatedLog.Text()
|
||||||
if accessLog {
|
if accessLog {
|
||||||
if len(line) > 0 {
|
if len(line) > 0 {
|
||||||
|
if !strings.Contains(line, "/api/rawdata") {
|
||||||
CheckAccessLogFormat(c, line, count)
|
CheckAccessLogFormat(c, line, count)
|
||||||
}
|
|
||||||
}
|
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,10 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
defer cmd.Process.Kill()
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
// wait for Traefik
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("rest@internal"))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
// Expected a 404 as we did not configure anything.
|
// Expected a 404 as we did not configure anything.
|
||||||
err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
|
err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
@ -44,15 +48,15 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) {
|
||||||
config: &dynamic.Configuration{
|
config: &dynamic.Configuration{
|
||||||
HTTP: &dynamic.HTTPConfiguration{
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
Routers: map[string]*dynamic.Router{
|
Routers: map[string]*dynamic.Router{
|
||||||
"router1": {
|
"routerHTTP": {
|
||||||
EntryPoints: []string{"web"},
|
EntryPoints: []string{"web"},
|
||||||
Middlewares: []string{},
|
Middlewares: []string{},
|
||||||
Service: "service1",
|
Service: "serviceHTTP",
|
||||||
Rule: "PathPrefix(`/`)",
|
Rule: "PathPrefix(`/`)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.Service{
|
Services: map[string]*dynamic.Service{
|
||||||
"service1": {
|
"serviceHTTP": {
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
Servers: []dynamic.Server{
|
Servers: []dynamic.Server{
|
||||||
{
|
{
|
||||||
|
@ -71,14 +75,14 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) {
|
||||||
config: &dynamic.Configuration{
|
config: &dynamic.Configuration{
|
||||||
TCP: &dynamic.TCPConfiguration{
|
TCP: &dynamic.TCPConfiguration{
|
||||||
Routers: map[string]*dynamic.TCPRouter{
|
Routers: map[string]*dynamic.TCPRouter{
|
||||||
"router1": {
|
"routerTCP": {
|
||||||
EntryPoints: []string{"web"},
|
EntryPoints: []string{"web"},
|
||||||
Service: "service1",
|
Service: "serviceTCP",
|
||||||
Rule: "HostSNI(`*`)",
|
Rule: "HostSNI(`*`)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: map[string]*dynamic.TCPService{
|
Services: map[string]*dynamic.TCPService{
|
||||||
"service1": {
|
"serviceTCP": {
|
||||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||||
Servers: []dynamic.TCPServer{
|
Servers: []dynamic.TCPServer{
|
||||||
{
|
{
|
||||||
|
@ -95,17 +99,17 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCase {
|
for _, test := range testCase {
|
||||||
json, err := json.Marshal(test.config)
|
data, err := json.Marshal(test.config)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8080/api/providers/rest", bytes.NewReader(json))
|
request, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8080/api/providers/rest", bytes.NewReader(data))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
response, err := http.DefaultClient.Do(request)
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(response.StatusCode, checker.Equals, http.StatusOK)
|
c.Assert(response.StatusCode, checker.Equals, http.StatusOK)
|
||||||
|
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains(test.ruleMatch))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 3*time.Second, try.BodyContains(test.ruleMatch))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
||||||
|
|
73
integration/testdata/rawdata-crd.json
vendored
73
integration/testdata/rawdata-crd.json
vendored
|
@ -1,5 +1,33 @@
|
||||||
{
|
{
|
||||||
"routers": {
|
"routers": {
|
||||||
|
"api@internal": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "api@internal",
|
||||||
|
"rule": "PathPrefix(`/api`)",
|
||||||
|
"priority": 9223372036854775806,
|
||||||
|
"status": "enabled",
|
||||||
|
"using": [
|
||||||
|
"traefik"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard@internal": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"middlewares": [
|
||||||
|
"dashboard_redirect@internal",
|
||||||
|
"dashboard_stripprefix@internal"
|
||||||
|
],
|
||||||
|
"service": "dashboard@internal",
|
||||||
|
"rule": "PathPrefix(`/`)",
|
||||||
|
"priority": 9223372036854775805,
|
||||||
|
"status": "enabled",
|
||||||
|
"using": [
|
||||||
|
"traefik"
|
||||||
|
]
|
||||||
|
},
|
||||||
"default-test.route-6b204d94623b3df4370c@kubernetescrd": {
|
"default-test.route-6b204d94623b3df4370c@kubernetescrd": {
|
||||||
"entryPoints": [
|
"entryPoints": [
|
||||||
"web"
|
"web"
|
||||||
|
@ -31,6 +59,29 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
|
"dashboard_redirect@internal": {
|
||||||
|
"redirectRegex": {
|
||||||
|
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
||||||
|
"replacement": "${1}/dashboard/",
|
||||||
|
"permanent": true
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard_stripprefix@internal": {
|
||||||
|
"stripPrefix": {
|
||||||
|
"prefixes": [
|
||||||
|
"/dashboard/",
|
||||||
|
"/dashboard"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
"default-mychain@kubernetescrd": {
|
"default-mychain@kubernetescrd": {
|
||||||
"chain": {
|
"chain": {
|
||||||
"middlewares": [
|
"middlewares": [
|
||||||
|
@ -52,11 +103,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
"api@internal": {
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"api@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard@internal": {
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
"default-test.route-6b204d94623b3df4370c@kubernetescrd": {
|
"default-test.route-6b204d94623b3df4370c@kubernetescrd": {
|
||||||
"loadBalancer": {
|
"loadBalancer": {
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"url": "http://10.42.0.2:80"
|
"url": "http://10.42.0.3:80"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "http://10.42.0.6:80"
|
"url": "http://10.42.0.6:80"
|
||||||
|
@ -69,7 +132,7 @@
|
||||||
"default-test.route-6b204d94623b3df4370c@kubernetescrd"
|
"default-test.route-6b204d94623b3df4370c@kubernetescrd"
|
||||||
],
|
],
|
||||||
"serverStatus": {
|
"serverStatus": {
|
||||||
"http://10.42.0.2:80": "UP",
|
"http://10.42.0.3:80": "UP",
|
||||||
"http://10.42.0.6:80": "UP"
|
"http://10.42.0.6:80": "UP"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -77,7 +140,7 @@
|
||||||
"loadBalancer": {
|
"loadBalancer": {
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"url": "http://10.42.0.2:80"
|
"url": "http://10.42.0.3:80"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "http://10.42.0.6:80"
|
"url": "http://10.42.0.6:80"
|
||||||
|
@ -90,7 +153,7 @@
|
||||||
"default-test2.route-23c7f4c450289ee29016@kubernetescrd"
|
"default-test2.route-23c7f4c450289ee29016@kubernetescrd"
|
||||||
],
|
],
|
||||||
"serverStatus": {
|
"serverStatus": {
|
||||||
"http://10.42.0.2:80": "UP",
|
"http://10.42.0.3:80": "UP",
|
||||||
"http://10.42.0.6:80": "UP"
|
"http://10.42.0.6:80": "UP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +181,7 @@
|
||||||
"terminationDelay": 100,
|
"terminationDelay": 100,
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"address": "10.42.0.4:8080"
|
"address": "10.42.0.2:8080"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "10.42.0.5:8080"
|
"address": "10.42.0.5:8080"
|
||||||
|
|
69
integration/testdata/rawdata-ingress.json
vendored
69
integration/testdata/rawdata-ingress.json
vendored
|
@ -1,5 +1,33 @@
|
||||||
{
|
{
|
||||||
"routers": {
|
"routers": {
|
||||||
|
"api@internal": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "api@internal",
|
||||||
|
"rule": "PathPrefix(`/api`)",
|
||||||
|
"priority": 9223372036854775806,
|
||||||
|
"status": "enabled",
|
||||||
|
"using": [
|
||||||
|
"traefik"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard@internal": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"middlewares": [
|
||||||
|
"dashboard_redirect@internal",
|
||||||
|
"dashboard_stripprefix@internal"
|
||||||
|
],
|
||||||
|
"service": "dashboard@internal",
|
||||||
|
"rule": "PathPrefix(`/`)",
|
||||||
|
"priority": 9223372036854775805,
|
||||||
|
"status": "enabled",
|
||||||
|
"using": [
|
||||||
|
"traefik"
|
||||||
|
]
|
||||||
|
},
|
||||||
"whoami-test-https-whoami-tls@kubernetes": {
|
"whoami-test-https-whoami-tls@kubernetes": {
|
||||||
"service": "default-whoami-http",
|
"service": "default-whoami-http",
|
||||||
"rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)",
|
"rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)",
|
||||||
|
@ -29,7 +57,44 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"middlewares": {
|
||||||
|
"dashboard_redirect@internal": {
|
||||||
|
"redirectRegex": {
|
||||||
|
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
||||||
|
"replacement": "${1}/dashboard/",
|
||||||
|
"permanent": true
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard_stripprefix@internal": {
|
||||||
|
"stripPrefix": {
|
||||||
|
"prefixes": [
|
||||||
|
"/dashboard/",
|
||||||
|
"/dashboard"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
"api@internal": {
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"api@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dashboard@internal": {
|
||||||
|
"status": "enabled",
|
||||||
|
"usedBy": [
|
||||||
|
"dashboard@internal"
|
||||||
|
]
|
||||||
|
},
|
||||||
"default-whoami-http@kubernetes": {
|
"default-whoami-http@kubernetes": {
|
||||||
"loadBalancer": {
|
"loadBalancer": {
|
||||||
"servers": [
|
"servers": [
|
||||||
|
@ -37,7 +102,7 @@
|
||||||
"url": "http://10.42.0.2:80"
|
"url": "http://10.42.0.2:80"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "http://10.42.0.4:80"
|
"url": "http://10.42.0.3:80"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"passHostHeader": true
|
"passHostHeader": true
|
||||||
|
@ -50,7 +115,7 @@
|
||||||
],
|
],
|
||||||
"serverStatus": {
|
"serverStatus": {
|
||||||
"http://10.42.0.2:80": "UP",
|
"http://10.42.0.2:80": "UP",
|
||||||
"http://10.42.0.4:80": "UP"
|
"http://10.42.0.3:80": "UP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,12 +27,6 @@ func (g DashboardHandler) Append(router *mux.Router) {
|
||||||
http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound)
|
http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.Methods(http.MethodGet).
|
|
||||||
Path("/dashboard/status").
|
|
||||||
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
||||||
http.Redirect(response, request, "/dashboard/", http.StatusFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Methods(http.MethodGet).
|
router.Methods(http.MethodGet).
|
||||||
PathPrefix("/dashboard/").
|
PathPrefix("/dashboard/").
|
||||||
Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets)))
|
Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets)))
|
||||||
|
|
|
@ -46,21 +46,17 @@ type RunTimeRepresentation struct {
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
dashboard bool
|
dashboard bool
|
||||||
debug bool
|
debug bool
|
||||||
|
staticConfig static.Configuration
|
||||||
|
dashboardAssets *assetfs.AssetFS
|
||||||
|
|
||||||
// runtimeConfiguration is the data set used to create all the data representations exposed by the API.
|
// runtimeConfiguration is the data set used to create all the data representations exposed by the API.
|
||||||
runtimeConfiguration *runtime.Configuration
|
runtimeConfiguration *runtime.Configuration
|
||||||
staticConfig static.Configuration
|
|
||||||
// statistics *types.Statistics
|
|
||||||
// stats *thoasstats.Stats // FIXME stats
|
|
||||||
// StatsRecorder *middlewares.StatsRecorder // FIXME stats
|
|
||||||
dashboardAssets *assetfs.AssetFS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuilder returns a http.Handler builder based on runtime.Configuration
|
// NewBuilder returns a http.Handler builder based on runtime.Configuration
|
||||||
func NewBuilder(staticConfig static.Configuration) func(*runtime.Configuration) http.Handler {
|
func NewBuilder(staticConfig static.Configuration) func(*runtime.Configuration) http.Handler {
|
||||||
return func(configuration *runtime.Configuration) http.Handler {
|
return func(configuration *runtime.Configuration) http.Handler {
|
||||||
router := mux.NewRouter()
|
return New(staticConfig, configuration).createRouter()
|
||||||
New(staticConfig, configuration).Append(router)
|
|
||||||
return router
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +70,6 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
dashboard: staticConfig.API.Dashboard,
|
dashboard: staticConfig.API.Dashboard,
|
||||||
// statistics: staticConfig.API.Statistics,
|
|
||||||
dashboardAssets: staticConfig.API.DashboardAssets,
|
dashboardAssets: staticConfig.API.DashboardAssets,
|
||||||
runtimeConfiguration: rConfig,
|
runtimeConfiguration: rConfig,
|
||||||
staticConfig: staticConfig,
|
staticConfig: staticConfig,
|
||||||
|
@ -82,8 +77,10 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append add api routes on a router
|
// createRouter creates API routes and router.
|
||||||
func (h Handler) Append(router *mux.Router) {
|
func (h Handler) createRouter() *mux.Router {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
if h.debug {
|
if h.debug {
|
||||||
DebugHandler{}.Append(router)
|
DebugHandler{}.Append(router)
|
||||||
}
|
}
|
||||||
|
@ -108,15 +105,13 @@ func (h Handler) Append(router *mux.Router) {
|
||||||
router.Methods(http.MethodGet).Path("/api/tcp/services").HandlerFunc(h.getTCPServices)
|
router.Methods(http.MethodGet).Path("/api/tcp/services").HandlerFunc(h.getTCPServices)
|
||||||
router.Methods(http.MethodGet).Path("/api/tcp/services/{serviceID}").HandlerFunc(h.getTCPService)
|
router.Methods(http.MethodGet).Path("/api/tcp/services/{serviceID}").HandlerFunc(h.getTCPService)
|
||||||
|
|
||||||
// FIXME stats
|
|
||||||
// health route
|
|
||||||
// router.Methods(http.MethodGet).Path("/health").HandlerFunc(p.getHealthHandler)
|
|
||||||
|
|
||||||
version.Handler{}.Append(router)
|
version.Handler{}.Append(router)
|
||||||
|
|
||||||
if h.dashboard {
|
if h.dashboard {
|
||||||
DashboardHandler{Assets: h.dashboardAssets}.Append(router)
|
DashboardHandler{Assets: h.dashboardAssets}.Append(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) {
|
func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -199,10 +198,7 @@ func TestHandler_EntryPoints(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler := New(test.conf, &runtime.Configuration{})
|
handler := New(test.conf, &runtime.Configuration{})
|
||||||
router := mux.NewRouter()
|
server := httptest.NewServer(handler.createRouter())
|
||||||
handler.Append(router)
|
|
||||||
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -813,10 +812,7 @@ func TestHandler_HTTP(t *testing.T) {
|
||||||
rtConf.GetRoutersByEntryPoints(context.Background(), []string{"web"}, false)
|
rtConf.GetRoutersByEntryPoints(context.Background(), []string{"web"}, false)
|
||||||
|
|
||||||
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
|
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
|
||||||
router := mux.NewRouter()
|
server := httptest.NewServer(handler.createRouter())
|
||||||
handler.Append(router)
|
|
||||||
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/provider/rest"
|
"github.com/containous/traefik/v2/pkg/provider/rest"
|
||||||
"github.com/containous/traefik/v2/pkg/tracing/jaeger"
|
"github.com/containous/traefik/v2/pkg/tracing/jaeger"
|
||||||
"github.com/containous/traefik/v2/pkg/types"
|
"github.com/containous/traefik/v2/pkg/types"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -252,10 +251,7 @@ func TestHandler_Overview(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler := New(test.confStatic, &test.confDyn)
|
handler := New(test.confStatic, &test.confDyn)
|
||||||
router := mux.NewRouter()
|
server := httptest.NewServer(handler.createRouter())
|
||||||
handler.Append(router)
|
|
||||||
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -520,10 +519,7 @@ func TestHandler_TCP(t *testing.T) {
|
||||||
rtConf.GetTCPRoutersByEntryPoints(context.Background(), []string{"web"})
|
rtConf.GetTCPRoutersByEntryPoints(context.Background(), []string{"web"})
|
||||||
|
|
||||||
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
|
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
|
||||||
router := mux.NewRouter()
|
server := httptest.NewServer(handler.createRouter())
|
||||||
handler.Append(router)
|
|
||||||
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -137,10 +136,7 @@ func TestHandler_RawData(t *testing.T) {
|
||||||
|
|
||||||
rtConf.PopulateUsedBy()
|
rtConf.PopulateUsedBy()
|
||||||
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
|
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
|
||||||
router := mux.NewRouter()
|
server := httptest.NewServer(handler.createRouter())
|
||||||
handler.Append(router)
|
|
||||||
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
resp, err := http.DefaultClient.Get(server.URL + test.path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -177,9 +177,9 @@ func (c *Configuration) SetEffectiveConfiguration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.API != nil && c.API.Insecure) ||
|
if (c.API != nil && c.API.Insecure) ||
|
||||||
(c.Ping != nil && c.Ping.EntryPoint == DefaultInternalEntryPointName) ||
|
(c.Ping != nil && !c.Ping.ManualRouting && c.Ping.EntryPoint == DefaultInternalEntryPointName) ||
|
||||||
(c.Metrics != nil && c.Metrics.Prometheus != nil && c.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) ||
|
(c.Metrics != nil && c.Metrics.Prometheus != nil && !c.Metrics.Prometheus.ManualRouting && c.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) ||
|
||||||
(c.Providers.Rest != nil) {
|
(c.Providers != nil && c.Providers.Rest != nil && c.Providers.Rest.Insecure) {
|
||||||
if _, ok := c.EntryPoints[DefaultInternalEntryPointName]; !ok {
|
if _, ok := c.EntryPoints[DefaultInternalEntryPointName]; !ok {
|
||||||
ep := &EntryPoint{Address: ":8080"}
|
ep := &EntryPoint{Address: ":8080"}
|
||||||
ep.SetDefaults()
|
ep.SetDefaults()
|
||||||
|
|
|
@ -2,7 +2,6 @@ package metrics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -13,7 +12,6 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/safe"
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
"github.com/containous/traefik/v2/pkg/types"
|
"github.com/containous/traefik/v2/pkg/types"
|
||||||
"github.com/go-kit/kit/metrics"
|
"github.com/go-kit/kit/metrics"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
@ -63,11 +61,8 @@ var promState = newPrometheusState()
|
||||||
var promRegistry = stdprometheus.NewRegistry()
|
var promRegistry = stdprometheus.NewRegistry()
|
||||||
|
|
||||||
// PrometheusHandler exposes Prometheus routes.
|
// PrometheusHandler exposes Prometheus routes.
|
||||||
type PrometheusHandler struct{}
|
func PrometheusHandler() http.Handler {
|
||||||
|
return promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{})
|
||||||
// Append adds Prometheus routes on a router.
|
|
||||||
func (h PrometheusHandler) Append(router *mux.Router) {
|
|
||||||
router.Methods(http.MethodGet).Path("/metrics").Handler(promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPrometheus registers all Prometheus metrics.
|
// RegisterPrometheus registers all Prometheus metrics.
|
||||||
|
@ -216,21 +211,22 @@ func registerPromState(ctx context.Context) bool {
|
||||||
// OnConfigurationUpdate receives the current configuration from Traefik.
|
// OnConfigurationUpdate receives the current configuration from Traefik.
|
||||||
// It then converts the configuration to the optimized package internal format
|
// It then converts the configuration to the optimized package internal format
|
||||||
// and sets it to the promState.
|
// and sets it to the promState.
|
||||||
func OnConfigurationUpdate(dynConf dynamic.Configurations, entryPoints []string) {
|
func OnConfigurationUpdate(conf dynamic.Configuration, entryPoints []string) {
|
||||||
dynamicConfig := newDynamicConfig()
|
dynamicConfig := newDynamicConfig()
|
||||||
|
|
||||||
for _, value := range entryPoints {
|
for _, value := range entryPoints {
|
||||||
dynamicConfig.entryPoints[value] = true
|
dynamicConfig.entryPoints[value] = true
|
||||||
}
|
}
|
||||||
for key, config := range dynConf {
|
|
||||||
for name := range config.HTTP.Routers {
|
for name := range conf.HTTP.Routers {
|
||||||
dynamicConfig.routers[fmt.Sprintf("%s@%s", name, key)] = true
|
dynamicConfig.routers[name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for serviceName, service := range config.HTTP.Services {
|
for serviceName, service := range conf.HTTP.Services {
|
||||||
dynamicConfig.services[fmt.Sprintf("%s@%s", serviceName, key)] = make(map[string]bool)
|
dynamicConfig.services[serviceName] = make(map[string]bool)
|
||||||
|
if service.LoadBalancer != nil {
|
||||||
for _, server := range service.LoadBalancer.Servers {
|
for _, server := range service.LoadBalancer.Servers {
|
||||||
dynamicConfig.services[fmt.Sprintf("%s@%s", serviceName, key)][server.URL] = true
|
dynamicConfig.services[serviceName][server.URL] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,20 +281,19 @@ func TestPrometheusMetricRemoval(t *testing.T) {
|
||||||
prometheusRegistry := RegisterPrometheus(context.Background(), &types.Prometheus{AddEntryPointsLabels: true, AddServicesLabels: true})
|
prometheusRegistry := RegisterPrometheus(context.Background(), &types.Prometheus{AddEntryPointsLabels: true, AddServicesLabels: true})
|
||||||
defer promRegistry.Unregister(promState)
|
defer promRegistry.Unregister(promState)
|
||||||
|
|
||||||
configurations := make(dynamic.Configurations)
|
conf := dynamic.Configuration{
|
||||||
configurations["providerName"] = &dynamic.Configuration{
|
|
||||||
HTTP: th.BuildConfiguration(
|
HTTP: th.BuildConfiguration(
|
||||||
th.WithRouters(
|
th.WithRouters(
|
||||||
th.WithRouter("foo",
|
th.WithRouter("foo@providerName",
|
||||||
th.WithServiceName("bar")),
|
th.WithServiceName("bar")),
|
||||||
),
|
),
|
||||||
th.WithLoadBalancerServices(th.WithService("bar",
|
th.WithLoadBalancerServices(th.WithService("bar@providerName",
|
||||||
th.WithServers(th.WithServer("http://localhost:9000"))),
|
th.WithServers(th.WithServer("http://localhost:9000"))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
OnConfigurationUpdate(configurations, []string{"entrypoint1"})
|
OnConfigurationUpdate(conf, []string{"entrypoint1"})
|
||||||
|
|
||||||
// Register some metrics manually that are not part of the active configuration.
|
// Register some metrics manually that are not part of the active configuration.
|
||||||
// Those metrics should be part of the /metrics output on the first scrape but
|
// Those metrics should be part of the /metrics output on the first scrape but
|
||||||
|
|
|
@ -4,13 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler expose ping routes.
|
// Handler expose ping routes.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
EntryPoint string `description:"EntryPoint" export:"true" json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"`
|
EntryPoint string `description:"EntryPoint" export:"true" json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"`
|
||||||
|
ManualRouting bool `description:"Manual routing" json:"manualRouting,omitempty" toml:"manualRouting,omitempty" yaml:"manualRouting,omitempty"`
|
||||||
terminating bool
|
terminating bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,15 +26,11 @@ func (h *Handler) WithContext(ctx context.Context) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append adds ping routes on a router.
|
func (h *Handler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
|
||||||
func (h *Handler) Append(router *mux.Router) {
|
|
||||||
router.Methods(http.MethodGet, http.MethodHead).Path("/ping").
|
|
||||||
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
||||||
statusCode := http.StatusOK
|
statusCode := http.StatusOK
|
||||||
if h.terminating {
|
if h.terminating {
|
||||||
statusCode = http.StatusServiceUnavailable
|
statusCode = http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
response.WriteHeader(statusCode)
|
response.WriteHeader(statusCode)
|
||||||
fmt.Fprint(response, http.StatusText(statusCode))
|
fmt.Fprint(response, http.StatusText(statusCode))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,11 @@ func (c *challengeHTTP) Timeout() (timeout, interval time.Duration) {
|
||||||
return 60 * time.Second, 5 * time.Second
|
return 60 * time.Second, 5 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append adds routes on internal router
|
// CreateHandler creates a HTTP handler to expose the token for the HTTP challenge.
|
||||||
func (p *Provider) Append(router *mux.Router) {
|
func (p *Provider) CreateHandler(notFoundHandler http.Handler) http.Handler {
|
||||||
|
router := mux.NewRouter().SkipClean(true)
|
||||||
|
router.NotFoundHandler = notFoundHandler
|
||||||
|
|
||||||
router.Methods(http.MethodGet).
|
router.Methods(http.MethodGet).
|
||||||
Path(http01.ChallengePath("{token}")).
|
Path(http01.ChallengePath("{token}")).
|
||||||
Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -64,6 +67,8 @@ func (p *Provider) Append(router *mux.Router) {
|
||||||
}
|
}
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte {
|
func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package rest
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
@ -23,8 +22,7 @@ type Provider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults sets the default values.
|
// SetDefaults sets the default values.
|
||||||
func (p *Provider) SetDefaults() {
|
func (p *Provider) SetDefaults() {}
|
||||||
}
|
|
||||||
|
|
||||||
var templatesRenderer = render.New(render.Options{Directory: "nowhere"})
|
var templatesRenderer = render.New(render.Options{Directory: "nowhere"})
|
||||||
|
|
||||||
|
@ -33,40 +31,32 @@ func (p *Provider) Init() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler creates an http.Handler for the Rest API
|
// CreateRouter creates a router for the Rest API
|
||||||
func (p *Provider) Handler() http.Handler {
|
func (p *Provider) CreateRouter() *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
p.Append(router)
|
router.Methods(http.MethodPut).Path("/api/providers/{provider}").Handler(p)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append add rest provider routes on a router.
|
func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
func (p *Provider) Append(systemRouter *mux.Router) {
|
vars := mux.Vars(req)
|
||||||
systemRouter.
|
|
||||||
Methods(http.MethodPut).
|
|
||||||
Path("/api/providers/{provider}").
|
|
||||||
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
||||||
vars := mux.Vars(request)
|
|
||||||
if vars["provider"] != "rest" {
|
if vars["provider"] != "rest" {
|
||||||
response.WriteHeader(http.StatusBadRequest)
|
http.Error(rw, "Only 'rest' provider can be updated through the REST API", http.StatusBadRequest)
|
||||||
fmt.Fprint(response, "Only 'rest' provider can be updated through the REST API")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration := new(dynamic.Configuration)
|
configuration := new(dynamic.Configuration)
|
||||||
body, _ := ioutil.ReadAll(request.Body)
|
|
||||||
|
|
||||||
if err := json.Unmarshal(body, configuration); err != nil {
|
if err := json.NewDecoder(req.Body).Decode(configuration); err != nil {
|
||||||
log.WithoutContext().Errorf("Error parsing configuration %+v", err)
|
log.WithoutContext().Errorf("Error parsing configuration %+v", err)
|
||||||
http.Error(response, fmt.Sprintf("%+v", err), http.StatusBadRequest)
|
http.Error(rw, fmt.Sprintf("%+v", err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.configurationChan <- dynamic.Message{ProviderName: "rest", Configuration: configuration}
|
p.configurationChan <- dynamic.Message{ProviderName: "rest", Configuration: configuration}
|
||||||
if err := templatesRenderer.JSON(response, http.StatusOK, configuration); err != nil {
|
if err := templatesRenderer.JSON(rw, http.StatusOK, configuration); err != nil {
|
||||||
log.WithoutContext().Error(err)
|
log.WithoutContext().Error(err)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide allows the provider to provide configurations to traefik
|
// Provide allows the provider to provide configurations to traefik
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"api": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "api@internal",
|
||||||
|
"rule": "PathPrefix(`/api`)",
|
||||||
|
"priority": 9223372036854775806
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"middlewares": [
|
||||||
|
"dashboard_redirect@internal",
|
||||||
|
"dashboard_stripprefix@internal"
|
||||||
|
],
|
||||||
|
"service": "dashboard@internal",
|
||||||
|
"rule": "PathPrefix(`/`)",
|
||||||
|
"priority": 9223372036854775805
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"middlewares": {
|
||||||
|
"dashboard_redirect": {
|
||||||
|
"redirectRegex": {
|
||||||
|
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
||||||
|
"replacement": "${1}/dashboard/",
|
||||||
|
"permanent": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dashboard_stripprefix": {
|
||||||
|
"stripPrefix": {
|
||||||
|
"prefixes": [
|
||||||
|
"/dashboard/",
|
||||||
|
"/dashboard"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"api": {},
|
||||||
|
"dashboard": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"api": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "api@internal",
|
||||||
|
"rule": "PathPrefix(`/api`)",
|
||||||
|
"priority": 9223372036854775806
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"api": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
10
pkg/provider/traefik/fixtures/api_secure_with_dashboard.json
Normal file
10
pkg/provider/traefik/fixtures/api_secure_with_dashboard.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"services": {
|
||||||
|
"api": {},
|
||||||
|
"dashboard": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"services": {
|
||||||
|
"api": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
76
pkg/provider/traefik/fixtures/full_configuration.json
Normal file
76
pkg/provider/traefik/fixtures/full_configuration.json
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"api": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "api@internal",
|
||||||
|
"rule": "PathPrefix(`/api`)",
|
||||||
|
"priority": 9223372036854775806
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"middlewares": [
|
||||||
|
"dashboard_redirect@internal",
|
||||||
|
"dashboard_stripprefix@internal"
|
||||||
|
],
|
||||||
|
"service": "dashboard@internal",
|
||||||
|
"rule": "PathPrefix(`/`)",
|
||||||
|
"priority": 9223372036854775805
|
||||||
|
},
|
||||||
|
"ping": {
|
||||||
|
"entryPoints": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"service": "ping@internal",
|
||||||
|
"rule": "PathPrefix(`/ping`)",
|
||||||
|
"priority": 9223372036854775807
|
||||||
|
},
|
||||||
|
"prometheus": {
|
||||||
|
"entryPoints": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"service": "prometheus@internal",
|
||||||
|
"rule": "PathPrefix(`/metrics`)",
|
||||||
|
"priority": 9223372036854775807
|
||||||
|
},
|
||||||
|
"rest": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "rest@internal",
|
||||||
|
"rule": "PathPrefix(`/api/providers`)",
|
||||||
|
"priority": 9223372036854775807
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"middlewares": {
|
||||||
|
"dashboard_redirect": {
|
||||||
|
"redirectRegex": {
|
||||||
|
"regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$",
|
||||||
|
"replacement": "${1}/dashboard/",
|
||||||
|
"permanent": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dashboard_stripprefix": {
|
||||||
|
"stripPrefix": {
|
||||||
|
"prefixes": [
|
||||||
|
"/dashboard/",
|
||||||
|
"/dashboard"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"api": {},
|
||||||
|
"dashboard": {},
|
||||||
|
"ping": {},
|
||||||
|
"prometheus": {},
|
||||||
|
"rest": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
13
pkg/provider/traefik/fixtures/full_configuration_secure.json
Normal file
13
pkg/provider/traefik/fixtures/full_configuration_secure.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"services": {
|
||||||
|
"api": {},
|
||||||
|
"dashboard": {},
|
||||||
|
"ping": {},
|
||||||
|
"prometheus": {},
|
||||||
|
"rest": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
9
pkg/provider/traefik/fixtures/ping_custom.json
Normal file
9
pkg/provider/traefik/fixtures/ping_custom.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"services": {
|
||||||
|
"ping": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
19
pkg/provider/traefik/fixtures/ping_simple.json
Normal file
19
pkg/provider/traefik/fixtures/ping_simple.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"ping": {
|
||||||
|
"entryPoints": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"service": "ping@internal",
|
||||||
|
"rule": "PathPrefix(`/ping`)",
|
||||||
|
"priority": 9223372036854775807
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"ping": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
9
pkg/provider/traefik/fixtures/prometheus_custom.json
Normal file
9
pkg/provider/traefik/fixtures/prometheus_custom.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"services": {
|
||||||
|
"prometheus": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
19
pkg/provider/traefik/fixtures/prometheus_simple.json
Normal file
19
pkg/provider/traefik/fixtures/prometheus_simple.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"prometheus": {
|
||||||
|
"entryPoints": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"service": "prometheus@internal",
|
||||||
|
"rule": "PathPrefix(`/metrics`)",
|
||||||
|
"priority": 9223372036854775807
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"prometheus": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
19
pkg/provider/traefik/fixtures/rest_insecure.json
Normal file
19
pkg/provider/traefik/fixtures/rest_insecure.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"rest": {
|
||||||
|
"entryPoints": [
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"service": "rest@internal",
|
||||||
|
"rule": "PathPrefix(`/api/providers`)",
|
||||||
|
"priority": 9223372036854775807
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"rest": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
9
pkg/provider/traefik/fixtures/rest_secure.json
Normal file
9
pkg/provider/traefik/fixtures/rest_secure.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"services": {
|
||||||
|
"rest": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
156
pkg/provider/traefik/internal.go
Normal file
156
pkg/provider/traefik/internal.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package traefik
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider"
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ provider.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
|
// Provider is a provider.Provider implementation that provides the internal routers.
|
||||||
|
type Provider struct {
|
||||||
|
staticCfg static.Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of the internal provider.
|
||||||
|
func New(staticCfg static.Configuration) *Provider {
|
||||||
|
return &Provider{staticCfg: staticCfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
|
||||||
|
func (i *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
|
||||||
|
configurationChan <- dynamic.Message{
|
||||||
|
ProviderName: "internal",
|
||||||
|
Configuration: i.createConfiguration(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the provider.
|
||||||
|
func (i *Provider) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Provider) createConfiguration() *dynamic.Configuration {
|
||||||
|
cfg := &dynamic.Configuration{
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Routers: make(map[string]*dynamic.Router),
|
||||||
|
Middlewares: make(map[string]*dynamic.Middleware),
|
||||||
|
Services: make(map[string]*dynamic.Service),
|
||||||
|
},
|
||||||
|
TCP: &dynamic.TCPConfiguration{
|
||||||
|
Routers: make(map[string]*dynamic.TCPRouter),
|
||||||
|
Services: make(map[string]*dynamic.TCPService),
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{
|
||||||
|
Stores: make(map[string]tls.Store),
|
||||||
|
Options: make(map[string]tls.Options),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
i.apiConfiguration(cfg)
|
||||||
|
i.pingConfiguration(cfg)
|
||||||
|
i.restConfiguration(cfg)
|
||||||
|
i.prometheusConfiguration(cfg)
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) {
|
||||||
|
if i.staticCfg.API == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.staticCfg.API.Insecure {
|
||||||
|
cfg.HTTP.Routers["api"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{"traefik"},
|
||||||
|
Service: "api@internal",
|
||||||
|
Priority: math.MaxInt64 - 1,
|
||||||
|
Rule: "PathPrefix(`/api`)",
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.staticCfg.API.Dashboard {
|
||||||
|
cfg.HTTP.Routers["dashboard"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{"traefik"},
|
||||||
|
Service: "dashboard@internal",
|
||||||
|
Priority: math.MaxInt64 - 2,
|
||||||
|
Rule: "PathPrefix(`/`)",
|
||||||
|
Middlewares: []string{"dashboard_redirect@internal", "dashboard_stripprefix@internal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Middlewares["dashboard_redirect"] = &dynamic.Middleware{
|
||||||
|
RedirectRegex: &dynamic.RedirectRegex{
|
||||||
|
Regex: `^(http:\/\/[^:]+(:\d+)?)/$`,
|
||||||
|
Replacement: "${1}/dashboard/",
|
||||||
|
Permanent: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cfg.HTTP.Middlewares["dashboard_stripprefix"] = &dynamic.Middleware{
|
||||||
|
StripPrefix: &dynamic.StripPrefix{Prefixes: []string{"/dashboard/", "/dashboard"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Services["api"] = &dynamic.Service{}
|
||||||
|
|
||||||
|
if i.staticCfg.API.Dashboard {
|
||||||
|
cfg.HTTP.Services["dashboard"] = &dynamic.Service{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Provider) pingConfiguration(cfg *dynamic.Configuration) {
|
||||||
|
if i.staticCfg.Ping == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !i.staticCfg.Ping.ManualRouting {
|
||||||
|
cfg.HTTP.Routers["ping"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{i.staticCfg.Ping.EntryPoint},
|
||||||
|
Service: "ping@internal",
|
||||||
|
Priority: math.MaxInt64,
|
||||||
|
Rule: "PathPrefix(`/ping`)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Services["ping"] = &dynamic.Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Provider) restConfiguration(cfg *dynamic.Configuration) {
|
||||||
|
if i.staticCfg.Providers == nil || i.staticCfg.Providers.Rest == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.staticCfg.Providers.Rest.Insecure {
|
||||||
|
cfg.HTTP.Routers["rest"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{"traefik"},
|
||||||
|
Service: "rest@internal",
|
||||||
|
Priority: math.MaxInt64,
|
||||||
|
Rule: "PathPrefix(`/api/providers`)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Services["rest"] = &dynamic.Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Provider) prometheusConfiguration(cfg *dynamic.Configuration) {
|
||||||
|
if i.staticCfg.Metrics == nil || i.staticCfg.Metrics.Prometheus == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !i.staticCfg.Metrics.Prometheus.ManualRouting {
|
||||||
|
cfg.HTTP.Routers["prometheus"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{i.staticCfg.Metrics.Prometheus.EntryPoint},
|
||||||
|
Service: "prometheus@internal",
|
||||||
|
Priority: math.MaxInt64,
|
||||||
|
Rule: "PathPrefix(`/metrics`)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Services["prometheus"] = &dynamic.Service{}
|
||||||
|
}
|
199
pkg/provider/traefik/internal_test.go
Normal file
199
pkg/provider/traefik/internal_test.go
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
package traefik
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/ping"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider/rest"
|
||||||
|
"github.com/containous/traefik/v2/pkg/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateExpected = flag.Bool("update_expected", false, "Update expected files in fixtures")
|
||||||
|
|
||||||
|
func Test_createConfiguration(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
staticCfg static.Configuration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "full_configuration.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
API: &static.API{
|
||||||
|
Insecure: true,
|
||||||
|
Dashboard: true,
|
||||||
|
},
|
||||||
|
Ping: &ping.Handler{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: false,
|
||||||
|
},
|
||||||
|
Providers: &static.Providers{
|
||||||
|
Rest: &rest.Provider{
|
||||||
|
Insecure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metrics: &types.Metrics{
|
||||||
|
Prometheus: &types.Prometheus{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "full_configuration_secure.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
API: &static.API{
|
||||||
|
Insecure: false,
|
||||||
|
Dashboard: true,
|
||||||
|
},
|
||||||
|
Ping: &ping.Handler{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: true,
|
||||||
|
},
|
||||||
|
Providers: &static.Providers{
|
||||||
|
Rest: &rest.Provider{
|
||||||
|
Insecure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metrics: &types.Metrics{
|
||||||
|
Prometheus: &types.Prometheus{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "api_insecure_with_dashboard.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
API: &static.API{
|
||||||
|
Insecure: true,
|
||||||
|
Dashboard: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "api_insecure_without_dashboard.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
API: &static.API{
|
||||||
|
Insecure: true,
|
||||||
|
Dashboard: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "api_secure_with_dashboard.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
API: &static.API{
|
||||||
|
Insecure: false,
|
||||||
|
Dashboard: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "api_secure_without_dashboard.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
API: &static.API{
|
||||||
|
Insecure: false,
|
||||||
|
Dashboard: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ping_simple.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
Ping: &ping.Handler{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ping_custom.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
Ping: &ping.Handler{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "rest_insecure.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
Providers: &static.Providers{
|
||||||
|
Rest: &rest.Provider{
|
||||||
|
Insecure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "rest_secure.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
Providers: &static.Providers{
|
||||||
|
Rest: &rest.Provider{
|
||||||
|
Insecure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "prometheus_simple.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
Metrics: &types.Metrics{
|
||||||
|
Prometheus: &types.Prometheus{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "prometheus_custom.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
Metrics: &types.Metrics{
|
||||||
|
Prometheus: &types.Prometheus{
|
||||||
|
EntryPoint: "test",
|
||||||
|
ManualRouting: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
provider := Provider{staticCfg: test.staticCfg}
|
||||||
|
|
||||||
|
cfg := provider.createConfiguration()
|
||||||
|
|
||||||
|
filename := filepath.Join("fixtures", test.desc)
|
||||||
|
|
||||||
|
if *updateExpected {
|
||||||
|
newJSON, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filename, newJSON, 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedJSON, err := ioutil.ReadFile(filename)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
actualJSON, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(expectedJSON), string(actualJSON))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,7 +67,7 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(defaultTLSOptionProviders) == 0 {
|
if len(defaultTLSOptionProviders) == 0 {
|
||||||
conf.TLS.Options["default"] = tls.Options{}
|
conf.TLS.Options["default"] = tls.DefaultTLSOptions
|
||||||
} else if len(defaultTLSOptionProviders) > 1 {
|
} else if len(defaultTLSOptionProviders) > 1 {
|
||||||
log.WithoutContext().Errorf("Default TLS Options defined multiple times in %v", defaultTLSOptionProviders)
|
log.WithoutContext().Errorf("Default TLS Options defined multiple times in %v", defaultTLSOptionProviders)
|
||||||
// We do not set an empty tls.TLS{} as above so that we actually get a "cascading failure" later on,
|
// We do not set an empty tls.TLS{} as above so that we actually get a "cascading failure" later on,
|
||||||
|
|
238
pkg/server/configurationwatcher.go
Normal file
238
pkg/server/configurationwatcher.go
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider"
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
"github.com/eapache/channels"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigurationWatcher watches configuration changes.
|
||||||
|
type ConfigurationWatcher struct {
|
||||||
|
provider provider.Provider
|
||||||
|
|
||||||
|
providersThrottleDuration time.Duration
|
||||||
|
|
||||||
|
currentConfigurations safe.Safe
|
||||||
|
|
||||||
|
configurationChan chan dynamic.Message
|
||||||
|
configurationValidatedChan chan dynamic.Message
|
||||||
|
providerConfigUpdateMap map[string]chan dynamic.Message
|
||||||
|
|
||||||
|
configurationListeners []func(dynamic.Configuration)
|
||||||
|
|
||||||
|
routinesPool *safe.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigurationWatcher creates a new ConfigurationWatcher.
|
||||||
|
func NewConfigurationWatcher(routinesPool *safe.Pool, pvd provider.Provider, providersThrottleDuration time.Duration) *ConfigurationWatcher {
|
||||||
|
watcher := &ConfigurationWatcher{
|
||||||
|
provider: pvd,
|
||||||
|
configurationChan: make(chan dynamic.Message, 100),
|
||||||
|
configurationValidatedChan: make(chan dynamic.Message, 100),
|
||||||
|
providerConfigUpdateMap: make(map[string]chan dynamic.Message),
|
||||||
|
providersThrottleDuration: providersThrottleDuration,
|
||||||
|
routinesPool: routinesPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConfigurations := make(dynamic.Configurations)
|
||||||
|
watcher.currentConfigurations.Set(currentConfigurations)
|
||||||
|
|
||||||
|
return watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the configuration watcher.
|
||||||
|
func (c *ConfigurationWatcher) Start() {
|
||||||
|
c.routinesPool.Go(func(stop chan bool) { c.listenProviders(stop) })
|
||||||
|
c.routinesPool.Go(func(stop chan bool) { c.listenConfigurations(stop) })
|
||||||
|
c.startProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the configuration watcher.
|
||||||
|
func (c *ConfigurationWatcher) Stop() {
|
||||||
|
close(c.configurationChan)
|
||||||
|
close(c.configurationValidatedChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddListener adds a new listener function used when new configuration is provided
|
||||||
|
func (c *ConfigurationWatcher) AddListener(listener func(dynamic.Configuration)) {
|
||||||
|
if c.configurationListeners == nil {
|
||||||
|
c.configurationListeners = make([]func(dynamic.Configuration), 0)
|
||||||
|
}
|
||||||
|
c.configurationListeners = append(c.configurationListeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationWatcher) startProvider() {
|
||||||
|
logger := log.WithoutContext()
|
||||||
|
|
||||||
|
jsonConf, err := json.Marshal(c.provider)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Unable to marshal provider configuration %T: %v", c.provider, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Starting provider %T %s", c.provider, jsonConf)
|
||||||
|
currentProvider := c.provider
|
||||||
|
|
||||||
|
safe.Go(func() {
|
||||||
|
err := currentProvider.Provide(c.configurationChan, c.routinesPool)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error starting provider %T: %s", currentProvider, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// listenProviders receives configuration changes from the providers.
|
||||||
|
// The configuration message then gets passed along a series of check
|
||||||
|
// to finally end up in a throttler that sends it to listenConfigurations (through c. configurationValidatedChan).
|
||||||
|
func (c *ConfigurationWatcher) listenProviders(stop chan bool) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case configMsg, ok := <-c.configurationChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if configMsg.Configuration == nil {
|
||||||
|
log.WithoutContext().WithField(log.ProviderName, configMsg.ProviderName).
|
||||||
|
Debug("Received nil configuration from provider, skipping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.preLoadConfiguration(configMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationWatcher) listenConfigurations(stop chan bool) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case configMsg, ok := <-c.configurationValidatedChan:
|
||||||
|
if !ok || configMsg.Configuration == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.loadMessage(configMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationWatcher) loadMessage(configMsg dynamic.Message) {
|
||||||
|
currentConfigurations := c.currentConfigurations.Get().(dynamic.Configurations)
|
||||||
|
|
||||||
|
// Copy configurations to new map so we don't change current if LoadConfig fails
|
||||||
|
newConfigurations := currentConfigurations.DeepCopy()
|
||||||
|
newConfigurations[configMsg.ProviderName] = configMsg.Configuration
|
||||||
|
|
||||||
|
c.currentConfigurations.Set(newConfigurations)
|
||||||
|
|
||||||
|
conf := mergeConfiguration(newConfigurations)
|
||||||
|
|
||||||
|
for _, listener := range c.configurationListeners {
|
||||||
|
listener(conf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) {
|
||||||
|
currentConfigurations := c.currentConfigurations.Get().(dynamic.Configurations)
|
||||||
|
|
||||||
|
logger := log.WithoutContext().WithField(log.ProviderName, configMsg.ProviderName)
|
||||||
|
if log.GetLevel() == logrus.DebugLevel {
|
||||||
|
copyConf := configMsg.Configuration.DeepCopy()
|
||||||
|
if copyConf.TLS != nil {
|
||||||
|
copyConf.TLS.Certificates = nil
|
||||||
|
|
||||||
|
for _, v := range copyConf.TLS.Stores {
|
||||||
|
v.DefaultCertificate = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonConf, err := json.Marshal(copyConf)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Could not marshal dynamic configuration: %v", err)
|
||||||
|
logger.Debugf("Configuration received from provider %s: [struct] %#v", configMsg.ProviderName, copyConf)
|
||||||
|
} else {
|
||||||
|
logger.Debugf("Configuration received from provider %s: %s", configMsg.ProviderName, string(jsonConf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEmptyConfiguration(configMsg.Configuration) {
|
||||||
|
logger.Infof("Skipping empty Configuration for provider %s", configMsg.ProviderName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
|
||||||
|
logger.Infof("Skipping same configuration for provider %s", configMsg.ProviderName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
providerConfigUpdateCh, ok := c.providerConfigUpdateMap[configMsg.ProviderName]
|
||||||
|
if !ok {
|
||||||
|
providerConfigUpdateCh = make(chan dynamic.Message)
|
||||||
|
c.providerConfigUpdateMap[configMsg.ProviderName] = providerConfigUpdateCh
|
||||||
|
c.routinesPool.Go(func(stop chan bool) {
|
||||||
|
c.throttleProviderConfigReload(c.providersThrottleDuration, c.configurationValidatedChan, providerConfigUpdateCh, stop)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
providerConfigUpdateCh <- configMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// throttleProviderConfigReload throttles the configuration reload speed for a single provider.
|
||||||
|
// It will immediately publish a new configuration and then only publish the next configuration after the throttle duration.
|
||||||
|
// Note that in the case it receives N new configs in the timeframe of the throttle duration after publishing,
|
||||||
|
// it will publish the last of the newly received configurations.
|
||||||
|
func (c *ConfigurationWatcher) throttleProviderConfigReload(throttle time.Duration, publish chan<- dynamic.Message, in <-chan dynamic.Message, stop chan bool) {
|
||||||
|
ring := channels.NewRingChannel(1)
|
||||||
|
defer ring.Close()
|
||||||
|
|
||||||
|
c.routinesPool.Go(func(stop chan bool) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case nextConfig := <-ring.Out():
|
||||||
|
if config, ok := nextConfig.(dynamic.Message); ok {
|
||||||
|
publish <- config
|
||||||
|
time.Sleep(throttle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case nextConfig := <-in:
|
||||||
|
ring.In() <- nextConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmptyConfiguration(conf *dynamic.Configuration) bool {
|
||||||
|
if conf == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.TCP == nil {
|
||||||
|
conf.TCP = &dynamic.TCPConfiguration{}
|
||||||
|
}
|
||||||
|
if conf.HTTP == nil {
|
||||||
|
conf.HTTP = &dynamic.HTTPConfiguration{}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpEmpty := conf.HTTP.Routers == nil && conf.HTTP.Services == nil && conf.HTTP.Middlewares == nil
|
||||||
|
tlsEmpty := conf.TLS == nil || conf.TLS.Certificates == nil && conf.TLS.Stores == nil && conf.TLS.Options == nil
|
||||||
|
tcpEmpty := conf.TCP.Routers == nil && conf.TCP.Services == nil
|
||||||
|
|
||||||
|
return httpEmpty && tlsEmpty && tcpEmpty
|
||||||
|
}
|
228
pkg/server/configurationwatcher_test.go
Normal file
228
pkg/server/configurationwatcher_test.go
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
th "github.com/containous/traefik/v2/pkg/testhelpers"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tls"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockProvider struct {
|
||||||
|
messages []dynamic.Message
|
||||||
|
wait time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
|
||||||
|
for _, message := range p.messages {
|
||||||
|
configurationChan <- message
|
||||||
|
|
||||||
|
wait := p.wait
|
||||||
|
if wait == 0 {
|
||||||
|
wait = 20 * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("wait", wait, time.Now().Nanosecond())
|
||||||
|
time.Sleep(wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mockProvider) Init() error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConfigurationWatcher(t *testing.T) {
|
||||||
|
routinesPool := safe.NewPool(context.Background())
|
||||||
|
pvd := &mockProvider{
|
||||||
|
messages: []dynamic.Message{{
|
||||||
|
ProviderName: "mock",
|
||||||
|
Configuration: &dynamic.Configuration{
|
||||||
|
HTTP: th.BuildConfiguration(
|
||||||
|
th.WithRouters(
|
||||||
|
th.WithRouter("test",
|
||||||
|
th.WithEntryPoints("e"),
|
||||||
|
th.WithServiceName("scv"))),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher := NewConfigurationWatcher(routinesPool, pvd, time.Second)
|
||||||
|
|
||||||
|
run := make(chan struct{})
|
||||||
|
|
||||||
|
watcher.AddListener(func(conf dynamic.Configuration) {
|
||||||
|
expected := dynamic.Configuration{
|
||||||
|
HTTP: th.BuildConfiguration(
|
||||||
|
th.WithRouters(
|
||||||
|
th.WithRouter("test@mock",
|
||||||
|
th.WithEntryPoints("e"),
|
||||||
|
th.WithServiceName("scv"))),
|
||||||
|
th.WithMiddlewares(),
|
||||||
|
th.WithLoadBalancerServices(),
|
||||||
|
),
|
||||||
|
TCP: &dynamic.TCPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.TCPRouter{},
|
||||||
|
Services: map[string]*dynamic.TCPService{},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{
|
||||||
|
Options: map[string]tls.Options{
|
||||||
|
"default": {},
|
||||||
|
},
|
||||||
|
Stores: map[string]tls.Store{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, conf)
|
||||||
|
close(run)
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.Start()
|
||||||
|
<-run
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenProvidersThrottleProviderConfigReload(t *testing.T) {
|
||||||
|
routinesPool := safe.NewPool(context.Background())
|
||||||
|
|
||||||
|
pvd := &mockProvider{
|
||||||
|
wait: 10 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
pvd.messages = append(pvd.messages, dynamic.Message{
|
||||||
|
ProviderName: "mock",
|
||||||
|
Configuration: &dynamic.Configuration{
|
||||||
|
HTTP: th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i))),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar")),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher := NewConfigurationWatcher(routinesPool, pvd, 30*time.Millisecond)
|
||||||
|
|
||||||
|
publishedConfigCount := 0
|
||||||
|
watcher.AddListener(func(_ dynamic.Configuration) {
|
||||||
|
publishedConfigCount++
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.Start()
|
||||||
|
defer watcher.Stop()
|
||||||
|
|
||||||
|
// give some time so that the configuration can be processed
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// after 50 milliseconds 5 new configs were published
|
||||||
|
// with a throttle duration of 30 milliseconds this means, we should have received 3 new configs
|
||||||
|
assert.Equal(t, 3, publishedConfigCount, "times configs were published")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenProvidersSkipsEmptyConfigs(t *testing.T) {
|
||||||
|
routinesPool := safe.NewPool(context.Background())
|
||||||
|
pvd := &mockProvider{
|
||||||
|
messages: []dynamic.Message{{ProviderName: "mock"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher := NewConfigurationWatcher(routinesPool, pvd, time.Second)
|
||||||
|
watcher.AddListener(func(_ dynamic.Configuration) {
|
||||||
|
t.Error("An empty configuration was published but it should not")
|
||||||
|
})
|
||||||
|
watcher.Start()
|
||||||
|
defer watcher.Stop()
|
||||||
|
|
||||||
|
// give some time so that the configuration can be processed
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) {
|
||||||
|
routinesPool := safe.NewPool(context.Background())
|
||||||
|
message := dynamic.Message{
|
||||||
|
ProviderName: "mock",
|
||||||
|
Configuration: &dynamic.Configuration{
|
||||||
|
HTTP: th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo")),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar")),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pvd := &mockProvider{
|
||||||
|
messages: []dynamic.Message{message, message},
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher := NewConfigurationWatcher(routinesPool, pvd, 0)
|
||||||
|
|
||||||
|
alreadyCalled := false
|
||||||
|
watcher.AddListener(func(_ dynamic.Configuration) {
|
||||||
|
if alreadyCalled {
|
||||||
|
t.Error("Same configuration should not be published multiple times")
|
||||||
|
}
|
||||||
|
alreadyCalled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.Start()
|
||||||
|
defer watcher.Stop()
|
||||||
|
|
||||||
|
// give some time so that the configuration can be processed
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
|
||||||
|
routinesPool := safe.NewPool(context.Background())
|
||||||
|
|
||||||
|
configuration := &dynamic.Configuration{
|
||||||
|
HTTP: th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo")),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar")),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
pvd := &mockProvider{
|
||||||
|
messages: []dynamic.Message{
|
||||||
|
{ProviderName: "mock", Configuration: configuration},
|
||||||
|
{ProviderName: "mock2", Configuration: configuration},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher := NewConfigurationWatcher(routinesPool, pvd, 0)
|
||||||
|
|
||||||
|
var publishedProviderConfig dynamic.Configuration
|
||||||
|
|
||||||
|
watcher.AddListener(func(conf dynamic.Configuration) {
|
||||||
|
publishedProviderConfig = conf
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.Start()
|
||||||
|
defer watcher.Stop()
|
||||||
|
|
||||||
|
// give some time so that the configuration can be processed
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
expected := dynamic.Configuration{
|
||||||
|
HTTP: th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo@mock"), th.WithRouter("foo@mock2")),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar@mock"), th.WithService("bar@mock2")),
|
||||||
|
th.WithMiddlewares(),
|
||||||
|
),
|
||||||
|
TCP: &dynamic.TCPConfiguration{
|
||||||
|
Routers: map[string]*dynamic.TCPRouter{},
|
||||||
|
Services: map[string]*dynamic.TCPService{},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{
|
||||||
|
Options: map[string]tls.Options{
|
||||||
|
"default": {},
|
||||||
|
},
|
||||||
|
Stores: map[string]tls.Store{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, publishedProviderConfig)
|
||||||
|
}
|
115
pkg/server/internal/provider_test.go
Normal file
115
pkg/server/internal/provider_test.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddProviderInContext(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
ctx context.Context
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "without provider information",
|
||||||
|
ctx: context.Background(),
|
||||||
|
name: "test",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "provider name embedded in element name",
|
||||||
|
ctx: context.Background(),
|
||||||
|
name: "test@foo",
|
||||||
|
expected: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "provider name in context",
|
||||||
|
ctx: context.WithValue(context.Background(), providerKey, "foo"),
|
||||||
|
name: "test",
|
||||||
|
expected: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "provider name in context and different provider name embedded in element name",
|
||||||
|
ctx: context.WithValue(context.Background(), providerKey, "foo"),
|
||||||
|
name: "test@fii",
|
||||||
|
expected: "fii",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "provider name in context and same provider name embedded in element name",
|
||||||
|
ctx: context.WithValue(context.Background(), providerKey, "foo"),
|
||||||
|
name: "test@foo",
|
||||||
|
expected: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
newCtx := AddProviderInContext(test.ctx, test.name)
|
||||||
|
|
||||||
|
var providerName string
|
||||||
|
if name, ok := newCtx.Value(providerKey).(string); ok {
|
||||||
|
providerName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, providerName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetQualifiedName(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
ctx context.Context
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "empty name",
|
||||||
|
ctx: context.Background(),
|
||||||
|
name: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "without provider",
|
||||||
|
ctx: context.Background(),
|
||||||
|
name: "test",
|
||||||
|
expected: "test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with explicit provider",
|
||||||
|
ctx: context.Background(),
|
||||||
|
name: "test@foo",
|
||||||
|
expected: "test@foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with provider in context",
|
||||||
|
ctx: context.WithValue(context.Background(), providerKey, "foo"),
|
||||||
|
name: "test",
|
||||||
|
expected: "test@foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with provider in context and explicit name",
|
||||||
|
ctx: context.WithValue(context.Background(), providerKey, "foo"),
|
||||||
|
name: "test@fii",
|
||||||
|
expected: "test@fii",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
qualifiedName := GetQualifiedName(test.ctx, test.name)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, qualifiedName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
124
pkg/server/middleware/chainbuilder.go
Normal file
124
pkg/server/middleware/chainbuilder.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/containous/alice"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
"github.com/containous/traefik/v2/pkg/metrics"
|
||||||
|
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
||||||
|
metricsmiddleware "github.com/containous/traefik/v2/pkg/middlewares/metrics"
|
||||||
|
"github.com/containous/traefik/v2/pkg/middlewares/requestdecorator"
|
||||||
|
mTracing "github.com/containous/traefik/v2/pkg/middlewares/tracing"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tracing"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tracing/jaeger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChainBuilder Creates a middleware chain by entry point. It is used for middlewares that are created almost systematically and that need to be created before all others.
|
||||||
|
type ChainBuilder struct {
|
||||||
|
metricsRegistry metrics.Registry
|
||||||
|
accessLoggerMiddleware *accesslog.Handler
|
||||||
|
tracer *tracing.Tracing
|
||||||
|
requestDecorator *requestdecorator.RequestDecorator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChainBuilder Creates a new ChainBuilder.
|
||||||
|
func NewChainBuilder(staticConfiguration static.Configuration, metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler) *ChainBuilder {
|
||||||
|
return &ChainBuilder{
|
||||||
|
metricsRegistry: metricsRegistry,
|
||||||
|
accessLoggerMiddleware: accessLoggerMiddleware,
|
||||||
|
tracer: setupTracing(staticConfiguration.Tracing),
|
||||||
|
requestDecorator: requestdecorator.New(staticConfiguration.HostResolver),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a middleware chain by entry point.
|
||||||
|
func (c *ChainBuilder) Build(ctx context.Context, entryPointName string) alice.Chain {
|
||||||
|
chain := alice.New()
|
||||||
|
|
||||||
|
if c.accessLoggerMiddleware != nil {
|
||||||
|
chain = chain.Append(accesslog.WrapHandler(c.accessLoggerMiddleware))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.tracer != nil {
|
||||||
|
chain = chain.Append(mTracing.WrapEntryPointHandler(ctx, c.tracer, entryPointName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.metricsRegistry != nil && c.metricsRegistry.IsEpEnabled() {
|
||||||
|
chain = chain.Append(metricsmiddleware.WrapEntryPointHandler(ctx, c.metricsRegistry, entryPointName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.Append(requestdecorator.WrapHandler(c.requestDecorator))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close accessLogger and tracer.
|
||||||
|
func (c *ChainBuilder) Close() {
|
||||||
|
if c.accessLoggerMiddleware != nil {
|
||||||
|
if err := c.accessLoggerMiddleware.Close(); err != nil {
|
||||||
|
log.WithoutContext().Errorf("Could not close the access log file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.tracer != nil {
|
||||||
|
c.tracer.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTracing(conf *static.Tracing) *tracing.Tracing {
|
||||||
|
if conf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var backend tracing.Backend
|
||||||
|
|
||||||
|
if conf.Jaeger != nil {
|
||||||
|
backend = conf.Jaeger
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Zipkin != nil {
|
||||||
|
if backend != nil {
|
||||||
|
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Zipkin backend.")
|
||||||
|
} else {
|
||||||
|
backend = conf.Zipkin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Datadog != nil {
|
||||||
|
if backend != nil {
|
||||||
|
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Datadog backend.")
|
||||||
|
} else {
|
||||||
|
backend = conf.Datadog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Instana != nil {
|
||||||
|
if backend != nil {
|
||||||
|
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Instana backend.")
|
||||||
|
} else {
|
||||||
|
backend = conf.Instana
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Haystack != nil {
|
||||||
|
if backend != nil {
|
||||||
|
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Haystack backend.")
|
||||||
|
} else {
|
||||||
|
backend = conf.Haystack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if backend == nil {
|
||||||
|
log.WithoutContext().Debug("Could not initialize tracing, using Jaeger by default")
|
||||||
|
defaultBackend := &jaeger.Config{}
|
||||||
|
defaultBackend.SetDefaults()
|
||||||
|
backend = defaultBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
tracer, err := tracing.NewTracing(conf.ServiceName, conf.SpanNameLimit, backend)
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Warnf("Unable to create tracer: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tracer
|
||||||
|
}
|
|
@ -1,89 +0,0 @@
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/containous/alice"
|
|
||||||
"github.com/containous/traefik/v2/pkg/api"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
|
||||||
"github.com/containous/traefik/v2/pkg/metrics"
|
|
||||||
"github.com/containous/traefik/v2/pkg/types"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewRouteAppenderAggregator Creates a new RouteAppenderAggregator
|
|
||||||
func NewRouteAppenderAggregator(ctx context.Context, conf static.Configuration,
|
|
||||||
entryPointName string, runtimeConfiguration *runtime.Configuration) *RouteAppenderAggregator {
|
|
||||||
aggregator := &RouteAppenderAggregator{}
|
|
||||||
|
|
||||||
if conf.Ping != nil && conf.Ping.EntryPoint == entryPointName {
|
|
||||||
aggregator.AddAppender(conf.Ping)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Metrics != nil && conf.Metrics.Prometheus != nil && conf.Metrics.Prometheus.EntryPoint == entryPointName {
|
|
||||||
aggregator.AddAppender(metrics.PrometheusHandler{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if entryPointName != "traefik" {
|
|
||||||
return aggregator
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Providers != nil && conf.Providers.Rest != nil && conf.Providers.Rest.Insecure {
|
|
||||||
aggregator.AddAppender(conf.Providers.Rest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.API != nil && conf.API.Insecure {
|
|
||||||
aggregator.AddAppender(api.New(conf, runtimeConfiguration))
|
|
||||||
}
|
|
||||||
|
|
||||||
return aggregator
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouteAppenderAggregator RouteAppender that aggregate other RouteAppender
|
|
||||||
type RouteAppenderAggregator struct {
|
|
||||||
appenders []types.RouteAppender
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append Adds routes to the router
|
|
||||||
func (r *RouteAppenderAggregator) Append(systemRouter *mux.Router) {
|
|
||||||
for _, router := range r.appenders {
|
|
||||||
router.Append(systemRouter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAppender adds a router in the aggregator
|
|
||||||
func (r *RouteAppenderAggregator) AddAppender(router types.RouteAppender) {
|
|
||||||
r.appenders = append(r.appenders, router)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMiddleware router with internal middleware
|
|
||||||
type WithMiddleware struct {
|
|
||||||
appender types.RouteAppender
|
|
||||||
routerMiddlewares *alice.Chain
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append Adds routes to the router
|
|
||||||
func (wm *WithMiddleware) Append(systemRouter *mux.Router) {
|
|
||||||
realRouter := systemRouter.PathPrefix("/").Subrouter()
|
|
||||||
|
|
||||||
wm.appender.Append(realRouter)
|
|
||||||
|
|
||||||
if err := realRouter.Walk(wrapRoute(wm.routerMiddlewares)); err != nil {
|
|
||||||
log.WithoutContext().Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapRoute with middlewares
|
|
||||||
func wrapRoute(middlewares *alice.Chain) func(*mux.Route, *mux.Router, []*mux.Route) error {
|
|
||||||
return func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
|
||||||
handler, err := middlewares.Then(route.GetHandler())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
route.Handler(handler)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewRouteAppenderAggregator(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
staticConf static.Configuration
|
|
||||||
expected map[string]int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "Secure API",
|
|
||||||
staticConf: static.Configuration{
|
|
||||||
Global: &static.Global{},
|
|
||||||
API: &static.API{
|
|
||||||
Insecure: false,
|
|
||||||
},
|
|
||||||
EntryPoints: static.EntryPoints{
|
|
||||||
"traefik": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: map[string]int{
|
|
||||||
"/api/providers": http.StatusBadGateway,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Insecure API",
|
|
||||||
staticConf: static.Configuration{
|
|
||||||
Global: &static.Global{},
|
|
||||||
API: &static.API{
|
|
||||||
Insecure: true,
|
|
||||||
},
|
|
||||||
EntryPoints: static.EntryPoints{
|
|
||||||
"traefik": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: map[string]int{
|
|
||||||
"/api/rawdata": http.StatusOK,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
router := NewRouteAppenderAggregator(ctx, test.staticConf, "traefik", nil)
|
|
||||||
|
|
||||||
internalMuxRouter := mux.NewRouter()
|
|
||||||
router.Append(internalMuxRouter)
|
|
||||||
|
|
||||||
internalMuxRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
})
|
|
||||||
|
|
||||||
actual := make(map[string]int)
|
|
||||||
for calledURL := range test.expected {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
request := httptest.NewRequest(http.MethodGet, calledURL, nil)
|
|
||||||
internalMuxRouter.ServeHTTP(recorder, request)
|
|
||||||
actual[calledURL] = recorder.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, test.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
|
||||||
"github.com/containous/traefik/v2/pkg/provider/acme"
|
|
||||||
"github.com/containous/traefik/v2/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewRouteAppenderFactory Creates a new RouteAppenderFactory
|
|
||||||
func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider []*acme.Provider) *RouteAppenderFactory {
|
|
||||||
return &RouteAppenderFactory{
|
|
||||||
staticConfiguration: staticConfiguration,
|
|
||||||
entryPointName: entryPointName,
|
|
||||||
acmeProvider: acmeProvider,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouteAppenderFactory A factory of RouteAppender
|
|
||||||
type RouteAppenderFactory struct {
|
|
||||||
staticConfiguration static.Configuration
|
|
||||||
entryPointName string
|
|
||||||
acmeProvider []*acme.Provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppender Creates a new RouteAppender
|
|
||||||
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, runtimeConfiguration *runtime.Configuration) types.RouteAppender {
|
|
||||||
aggregator := NewRouteAppenderAggregator(ctx, r.staticConfiguration, r.entryPointName, runtimeConfiguration)
|
|
||||||
|
|
||||||
for _, p := range r.acmeProvider {
|
|
||||||
if p != nil && p.HTTPChallenge != nil && p.HTTPChallenge.EntryPoint == r.entryPointName {
|
|
||||||
aggregator.AddAppender(p)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return aggregator
|
|
||||||
}
|
|
|
@ -11,41 +11,55 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/recovery"
|
"github.com/containous/traefik/v2/pkg/middlewares/recovery"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/tracing"
|
"github.com/containous/traefik/v2/pkg/middlewares/tracing"
|
||||||
"github.com/containous/traefik/v2/pkg/responsemodifiers"
|
|
||||||
"github.com/containous/traefik/v2/pkg/rules"
|
"github.com/containous/traefik/v2/pkg/rules"
|
||||||
"github.com/containous/traefik/v2/pkg/server/internal"
|
"github.com/containous/traefik/v2/pkg/server/internal"
|
||||||
"github.com/containous/traefik/v2/pkg/server/middleware"
|
"github.com/containous/traefik/v2/pkg/server/middleware"
|
||||||
"github.com/containous/traefik/v2/pkg/server/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
recoveryMiddlewareName = "traefik-internal-recovery"
|
recoveryMiddlewareName = "traefik-internal-recovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type middlewareBuilder interface {
|
||||||
|
BuildChain(ctx context.Context, names []string) *alice.Chain
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseModifierBuilder interface {
|
||||||
|
Build(ctx context.Context, names []string) func(*http.Response) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceManager interface {
|
||||||
|
BuildHTTP(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error)
|
||||||
|
LaunchHealthCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager A route/router manager
|
||||||
|
type Manager struct {
|
||||||
|
routerHandlers map[string]http.Handler
|
||||||
|
serviceManager serviceManager
|
||||||
|
middlewaresBuilder middlewareBuilder
|
||||||
|
chainBuilder *middleware.ChainBuilder
|
||||||
|
modifierBuilder responseModifierBuilder
|
||||||
|
conf *runtime.Configuration
|
||||||
|
}
|
||||||
|
|
||||||
// NewManager Creates a new Manager
|
// NewManager Creates a new Manager
|
||||||
func NewManager(conf *runtime.Configuration,
|
func NewManager(conf *runtime.Configuration,
|
||||||
serviceManager *service.Manager,
|
serviceManager serviceManager,
|
||||||
middlewaresBuilder *middleware.Builder,
|
middlewaresBuilder middlewareBuilder,
|
||||||
modifierBuilder *responsemodifiers.Builder,
|
modifierBuilder responseModifierBuilder,
|
||||||
|
chainBuilder *middleware.ChainBuilder,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
routerHandlers: make(map[string]http.Handler),
|
routerHandlers: make(map[string]http.Handler),
|
||||||
serviceManager: serviceManager,
|
serviceManager: serviceManager,
|
||||||
middlewaresBuilder: middlewaresBuilder,
|
middlewaresBuilder: middlewaresBuilder,
|
||||||
modifierBuilder: modifierBuilder,
|
modifierBuilder: modifierBuilder,
|
||||||
|
chainBuilder: chainBuilder,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager A route/router manager
|
|
||||||
type Manager struct {
|
|
||||||
routerHandlers map[string]http.Handler
|
|
||||||
serviceManager *service.Manager
|
|
||||||
middlewaresBuilder *middleware.Builder
|
|
||||||
modifierBuilder *responsemodifiers.Builder
|
|
||||||
conf *runtime.Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
|
func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
|
||||||
if m.conf != nil {
|
if m.conf != nil {
|
||||||
return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
|
return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
|
||||||
|
@ -79,6 +93,22 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, entryPointName := range entryPoints {
|
||||||
|
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
|
||||||
|
|
||||||
|
handler, ok := entryPointHandlers[entryPointName]
|
||||||
|
if !ok || handler == nil {
|
||||||
|
handler = BuildDefaultHTTPRouter()
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerWithMiddlewares, err := m.chainBuilder.Build(ctx, entryPointName).Then(handler)
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entryPointHandlers[entryPointName] = handlerWithMiddlewares
|
||||||
|
}
|
||||||
|
|
||||||
m.serviceManager.LaunchHealthCheck()
|
m.serviceManager.LaunchHealthCheck()
|
||||||
|
|
||||||
return entryPointHandlers
|
return entryPointHandlers
|
||||||
|
@ -167,3 +197,8 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
|
||||||
|
|
||||||
return alice.New().Extend(*mHandler).Append(tHandler).Then(sHandler)
|
return alice.New().Extend(*mHandler).Append(tHandler).Then(sHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildDefaultHTTPRouter creates a default HTTP router.
|
||||||
|
func BuildDefaultHTTPRouter() http.Handler {
|
||||||
|
return http.NotFoundHandler()
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/requestdecorator"
|
"github.com/containous/traefik/v2/pkg/middlewares/requestdecorator"
|
||||||
"github.com/containous/traefik/v2/pkg/responsemodifiers"
|
"github.com/containous/traefik/v2/pkg/responsemodifiers"
|
||||||
|
@ -24,7 +25,7 @@ import (
|
||||||
func TestRouterManager_Get(t *testing.T) {
|
func TestRouterManager_Get(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
|
|
||||||
type ExpectedResult struct {
|
type expectedResult struct {
|
||||||
StatusCode int
|
StatusCode int
|
||||||
RequestHeaders map[string]string
|
RequestHeaders map[string]string
|
||||||
}
|
}
|
||||||
|
@ -35,7 +36,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
serviceConfig map[string]*dynamic.Service
|
serviceConfig map[string]*dynamic.Service
|
||||||
middlewaresConfig map[string]*dynamic.Middleware
|
middlewaresConfig map[string]*dynamic.Middleware
|
||||||
entryPoints []string
|
entryPoints []string
|
||||||
expected ExpectedResult
|
expected expectedResult
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "no middleware",
|
desc: "no middleware",
|
||||||
|
@ -58,7 +59,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
expected: expectedResult{StatusCode: http.StatusOK},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "no load balancer",
|
desc: "no load balancer",
|
||||||
|
@ -73,7 +74,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
"foo-service": {},
|
"foo-service": {},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{StatusCode: http.StatusNotFound},
|
expected: expectedResult{StatusCode: http.StatusNotFound},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "no middleware, default entry point",
|
desc: "no middleware, default entry point",
|
||||||
|
@ -95,7 +96,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
expected: expectedResult{StatusCode: http.StatusOK},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "no middleware, no matching",
|
desc: "no middleware, no matching",
|
||||||
|
@ -118,7 +119,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{StatusCode: http.StatusNotFound},
|
expected: expectedResult{StatusCode: http.StatusNotFound},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "middleware: headers > auth",
|
desc: "middleware: headers > auth",
|
||||||
|
@ -154,7 +155,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{
|
expected: expectedResult{
|
||||||
StatusCode: http.StatusUnauthorized,
|
StatusCode: http.StatusUnauthorized,
|
||||||
RequestHeaders: map[string]string{
|
RequestHeaders: map[string]string{
|
||||||
"X-Apero": "beer",
|
"X-Apero": "beer",
|
||||||
|
@ -195,7 +196,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{
|
expected: expectedResult{
|
||||||
StatusCode: http.StatusUnauthorized,
|
StatusCode: http.StatusUnauthorized,
|
||||||
RequestHeaders: map[string]string{
|
RequestHeaders: map[string]string{
|
||||||
"X-Apero": "",
|
"X-Apero": "",
|
||||||
|
@ -223,7 +224,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
expected: expectedResult{StatusCode: http.StatusOK},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "no middleware with specified provider name",
|
desc: "no middleware with specified provider name",
|
||||||
|
@ -246,7 +247,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
expected: expectedResult{StatusCode: http.StatusOK},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "middleware: chain with provider name",
|
desc: "middleware: chain with provider name",
|
||||||
|
@ -285,7 +286,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryPoints: []string{"web"},
|
entryPoints: []string{"web"},
|
||||||
expected: ExpectedResult{
|
expected: expectedResult{
|
||||||
StatusCode: http.StatusUnauthorized,
|
StatusCode: http.StatusUnauthorized,
|
||||||
RequestHeaders: map[string]string{
|
RequestHeaders: map[string]string{
|
||||||
"X-Apero": "",
|
"X-Apero": "",
|
||||||
|
@ -306,10 +307,13 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
Middlewares: test.middlewaresConfig,
|
Middlewares: test.middlewaresConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil, nil, nil)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil)
|
||||||
|
|
||||||
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, chainBuilder)
|
||||||
|
|
||||||
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
|
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
|
||||||
|
|
||||||
|
@ -407,10 +411,12 @@ func TestAccessLog(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil, nil, nil)
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil)
|
||||||
|
|
||||||
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, chainBuilder)
|
||||||
|
|
||||||
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
|
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
|
||||||
|
|
||||||
|
@ -680,7 +686,6 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
test := test
|
test := test
|
||||||
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -693,10 +698,13 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
Middlewares: test.middlewareConfig,
|
Middlewares: test.middlewareConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil, nil, nil)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{})
|
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{})
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil)
|
||||||
|
|
||||||
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, chainBuilder)
|
||||||
|
|
||||||
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
|
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
|
||||||
|
|
||||||
|
@ -762,10 +770,13 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil, nil, nil)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{})
|
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{})
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil)
|
||||||
|
|
||||||
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, chainBuilder)
|
||||||
|
|
||||||
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
|
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
|
||||||
|
|
||||||
|
@ -790,6 +801,7 @@ func BenchmarkRouterServe(b *testing.B) {
|
||||||
StatusCode: 200,
|
StatusCode: 200,
|
||||||
Body: ioutil.NopCloser(strings.NewReader("")),
|
Body: ioutil.NopCloser(strings.NewReader("")),
|
||||||
}
|
}
|
||||||
|
|
||||||
routersConfig := map[string]*dynamic.Router{
|
routersConfig := map[string]*dynamic.Router{
|
||||||
"foo": {
|
"foo": {
|
||||||
EntryPoints: []string{"web"},
|
EntryPoints: []string{"web"},
|
||||||
|
@ -817,10 +829,13 @@ func BenchmarkRouterServe(b *testing.B) {
|
||||||
Middlewares: map[string]*dynamic.Middleware{},
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil, nil, nil)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil)
|
||||||
|
|
||||||
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, chainBuilder)
|
||||||
|
|
||||||
handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false)
|
handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false)
|
||||||
|
|
||||||
|
@ -857,7 +872,8 @@ func BenchmarkService(b *testing.B) {
|
||||||
Services: serviceConfig,
|
Services: serviceConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil, nil, nil)
|
|
||||||
|
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,11 @@ import (
|
||||||
traefiktls "github.com/containous/traefik/v2/pkg/tls"
|
traefiktls "github.com/containous/traefik/v2/pkg/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTLSConfigName = "default"
|
||||||
|
defaultTLSStoreName = "default"
|
||||||
|
)
|
||||||
|
|
||||||
// NewManager Creates a new Manager
|
// NewManager Creates a new Manager
|
||||||
func NewManager(conf *runtime.Configuration,
|
func NewManager(conf *runtime.Configuration,
|
||||||
serviceManager *tcpservice.Manager,
|
serviceManager *tcpservice.Manager,
|
||||||
|
@ -88,15 +93,18 @@ type nameAndConfig struct {
|
||||||
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) {
|
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) {
|
||||||
router := &tcp.Router{}
|
router := &tcp.Router{}
|
||||||
router.HTTPHandler(handlerHTTP)
|
router.HTTPHandler(handlerHTTP)
|
||||||
const defaultTLSConfigName = "default"
|
|
||||||
|
|
||||||
defaultTLSConf, err := m.tlsManager.Get("default", defaultTLSConfigName)
|
defaultTLSConf, err := m.tlsManager.Get(defaultTLSStoreName, defaultTLSConfigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
|
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
|
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
|
||||||
|
|
||||||
|
if len(configsHTTP) > 0 {
|
||||||
|
router.AddRouteHTTPTLS("*", defaultTLSConf)
|
||||||
|
}
|
||||||
|
|
||||||
// Keyed by domain, then by options reference.
|
// Keyed by domain, then by options reference.
|
||||||
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
|
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
|
||||||
for routerHTTPName, routerHTTPConfig := range configsHTTP {
|
for routerHTTPName, routerHTTPConfig := range configsHTTP {
|
||||||
|
@ -126,12 +134,13 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
tlsOptionsName = internal.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
|
tlsOptionsName = internal.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf, err := m.tlsManager.Get("default", tlsOptionsName)
|
tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routerHTTPConfig.AddError(err, true)
|
routerHTTPConfig.AddError(err, true)
|
||||||
logger.Debug(err)
|
logger.Debug(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if tlsOptionsForHostSNI[domain] == nil {
|
if tlsOptionsForHostSNI[domain] == nil {
|
||||||
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
|
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
|
||||||
}
|
}
|
||||||
|
@ -153,7 +162,9 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
config = v.TLSConfig
|
config = v.TLSConfig
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
|
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
|
||||||
|
|
||||||
router.AddRouteHTTPTLS(hostSNI, config)
|
router.AddRouteHTTPTLS(hostSNI, config)
|
||||||
} else {
|
} else {
|
||||||
routers := make([]string, 0, len(tlsConfigs))
|
routers := make([]string, 0, len(tlsConfigs))
|
||||||
|
@ -161,7 +172,9 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
|
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
|
||||||
routers = append(routers, v.routerName)
|
routers = append(routers, v.routerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
|
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
|
||||||
|
|
||||||
router.AddRouteHTTPTLS(hostSNI, defaultTLSConf)
|
router.AddRouteHTTPTLS(hostSNI, defaultTLSConf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,7 +229,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
tlsOptionsName = internal.GetQualifiedName(ctxRouter, tlsOptionsName)
|
tlsOptionsName = internal.GetQualifiedName(ctxRouter, tlsOptionsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf, err := m.tlsManager.Get("default", tlsOptionsName)
|
tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routerConfig.AddError(err, true)
|
routerConfig.AddError(err, true)
|
||||||
logger.Debug(err)
|
logger.Debug(err)
|
||||||
|
|
|
@ -2,166 +2,47 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/api"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
"github.com/containous/traefik/v2/pkg/metrics"
|
"github.com/containous/traefik/v2/pkg/metrics"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/requestdecorator"
|
|
||||||
"github.com/containous/traefik/v2/pkg/provider"
|
|
||||||
"github.com/containous/traefik/v2/pkg/safe"
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
"github.com/containous/traefik/v2/pkg/tls"
|
"github.com/containous/traefik/v2/pkg/server/middleware"
|
||||||
"github.com/containous/traefik/v2/pkg/tracing"
|
|
||||||
"github.com/containous/traefik/v2/pkg/tracing/jaeger"
|
|
||||||
"github.com/containous/traefik/v2/pkg/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is the reverse-proxy/load-balancer engine
|
// Server is the reverse-proxy/load-balancer engine
|
||||||
type Server struct {
|
type Server struct {
|
||||||
entryPointsTCP TCPEntryPoints
|
watcher *ConfigurationWatcher
|
||||||
configurationChan chan dynamic.Message
|
tcpEntryPoints TCPEntryPoints
|
||||||
configurationValidatedChan chan dynamic.Message
|
chainBuilder *middleware.ChainBuilder
|
||||||
|
|
||||||
|
accessLoggerMiddleware *accesslog.Handler
|
||||||
|
|
||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
stopChan chan bool
|
stopChan chan bool
|
||||||
currentConfigurations safe.Safe
|
|
||||||
providerConfigUpdateMap map[string]chan dynamic.Message
|
|
||||||
accessLoggerMiddleware *accesslog.Handler
|
|
||||||
tracer *tracing.Tracing
|
|
||||||
routinesPool *safe.Pool
|
routinesPool *safe.Pool
|
||||||
defaultRoundTripper http.RoundTripper
|
|
||||||
metricsRegistry metrics.Registry
|
|
||||||
provider provider.Provider
|
|
||||||
configurationListeners []func(dynamic.Configuration)
|
|
||||||
requestDecorator *requestdecorator.RequestDecorator
|
|
||||||
providersThrottleDuration time.Duration
|
|
||||||
tlsManager *tls.Manager
|
|
||||||
api func(configuration *runtime.Configuration) http.Handler
|
|
||||||
restHandler http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouteAppenderFactory the route appender factory interface
|
|
||||||
type RouteAppenderFactory interface {
|
|
||||||
NewAppender(ctx context.Context, runtimeConfiguration *runtime.Configuration) types.RouteAppender
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTracing(conf *static.Tracing) tracing.Backend {
|
|
||||||
var backend tracing.Backend
|
|
||||||
|
|
||||||
if conf.Jaeger != nil {
|
|
||||||
backend = conf.Jaeger
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Zipkin != nil {
|
|
||||||
if backend != nil {
|
|
||||||
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Zipkin backend.")
|
|
||||||
} else {
|
|
||||||
backend = conf.Zipkin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Datadog != nil {
|
|
||||||
if backend != nil {
|
|
||||||
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Datadog backend.")
|
|
||||||
} else {
|
|
||||||
backend = conf.Datadog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Instana != nil {
|
|
||||||
if backend != nil {
|
|
||||||
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Instana backend.")
|
|
||||||
} else {
|
|
||||||
backend = conf.Instana
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Haystack != nil {
|
|
||||||
if backend != nil {
|
|
||||||
log.WithoutContext().Error("Multiple tracing backend are not supported: cannot create Haystack backend.")
|
|
||||||
} else {
|
|
||||||
backend = conf.Haystack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if backend == nil {
|
|
||||||
log.WithoutContext().Debug("Could not initialize tracing, use Jaeger by default")
|
|
||||||
bcd := &jaeger.Config{}
|
|
||||||
bcd.SetDefaults()
|
|
||||||
backend = bcd
|
|
||||||
}
|
|
||||||
|
|
||||||
return backend
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer returns an initialized Server.
|
// NewServer returns an initialized Server.
|
||||||
func NewServer(staticConfiguration static.Configuration, provider provider.Provider, entryPoints TCPEntryPoints, tlsManager *tls.Manager) *Server {
|
func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, watcher *ConfigurationWatcher,
|
||||||
server := &Server{}
|
chainBuilder *middleware.ChainBuilder, accessLoggerMiddleware *accesslog.Handler) *Server {
|
||||||
|
srv := &Server{
|
||||||
if staticConfiguration.API != nil {
|
watcher: watcher,
|
||||||
server.api = api.NewBuilder(staticConfiguration)
|
tcpEntryPoints: entryPoints,
|
||||||
|
chainBuilder: chainBuilder,
|
||||||
|
accessLoggerMiddleware: accessLoggerMiddleware,
|
||||||
|
signals: make(chan os.Signal, 1),
|
||||||
|
stopChan: make(chan bool, 1),
|
||||||
|
routinesPool: routinesPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
if staticConfiguration.Providers != nil && staticConfiguration.Providers.Rest != nil {
|
srv.configureSignals()
|
||||||
server.restHandler = staticConfiguration.Providers.Rest.Handler()
|
|
||||||
}
|
|
||||||
|
|
||||||
server.provider = provider
|
return srv
|
||||||
server.entryPointsTCP = entryPoints
|
|
||||||
server.configurationChan = make(chan dynamic.Message, 100)
|
|
||||||
server.configurationValidatedChan = make(chan dynamic.Message, 100)
|
|
||||||
server.signals = make(chan os.Signal, 1)
|
|
||||||
server.stopChan = make(chan bool, 1)
|
|
||||||
server.configureSignals()
|
|
||||||
currentConfigurations := make(dynamic.Configurations)
|
|
||||||
server.currentConfigurations.Set(currentConfigurations)
|
|
||||||
server.providerConfigUpdateMap = make(map[string]chan dynamic.Message)
|
|
||||||
server.tlsManager = tlsManager
|
|
||||||
|
|
||||||
if staticConfiguration.Providers != nil {
|
|
||||||
server.providersThrottleDuration = time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
transport, err := createHTTPTransport(staticConfiguration.ServersTransport)
|
|
||||||
if err != nil {
|
|
||||||
log.WithoutContext().Errorf("Could not configure HTTP Transport, fallbacking on default transport: %v", err)
|
|
||||||
server.defaultRoundTripper = http.DefaultTransport
|
|
||||||
} else {
|
|
||||||
server.defaultRoundTripper = transport
|
|
||||||
}
|
|
||||||
|
|
||||||
server.routinesPool = safe.NewPool(context.Background())
|
|
||||||
|
|
||||||
if staticConfiguration.Tracing != nil {
|
|
||||||
tracingBackend := setupTracing(staticConfiguration.Tracing)
|
|
||||||
if tracingBackend != nil {
|
|
||||||
server.tracer, err = tracing.NewTracing(staticConfiguration.Tracing.ServiceName, staticConfiguration.Tracing.SpanNameLimit, tracingBackend)
|
|
||||||
if err != nil {
|
|
||||||
log.WithoutContext().Warnf("Unable to create tracer: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.requestDecorator = requestdecorator.New(staticConfiguration.HostResolver)
|
|
||||||
|
|
||||||
server.metricsRegistry = registerMetricClients(staticConfiguration.Metrics)
|
|
||||||
|
|
||||||
if staticConfiguration.AccessLog != nil {
|
|
||||||
var err error
|
|
||||||
server.accessLoggerMiddleware, err = accesslog.NewHandler(staticConfiguration.AccessLog)
|
|
||||||
if err != nil {
|
|
||||||
log.WithoutContext().Warnf("Unable to create access logger : %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the server and Stop/Close it when context is Done
|
// Start starts the server and Stop/Close it when context is Done
|
||||||
|
@ -175,20 +56,15 @@ func (s *Server) Start(ctx context.Context) {
|
||||||
s.Stop()
|
s.Stop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.startTCPServers()
|
s.tcpEntryPoints.Start()
|
||||||
s.routinesPool.Go(func(stop chan bool) {
|
s.watcher.Start()
|
||||||
s.listenProviders(stop)
|
|
||||||
})
|
|
||||||
s.routinesPool.Go(func(stop chan bool) {
|
|
||||||
s.listenConfigurations(stop)
|
|
||||||
})
|
|
||||||
s.startProvider()
|
|
||||||
s.routinesPool.Go(func(stop chan bool) {
|
s.routinesPool.Go(func(stop chan bool) {
|
||||||
s.listenSignals(stop)
|
s.listenSignals(stop)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait blocks until server is shutted down.
|
// Wait blocks until the server shutdown.
|
||||||
func (s *Server) Wait() {
|
func (s *Server) Wait() {
|
||||||
<-s.stopChan
|
<-s.stopChan
|
||||||
}
|
}
|
||||||
|
@ -197,25 +73,15 @@ func (s *Server) Wait() {
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
defer log.WithoutContext().Info("Server stopped")
|
defer log.WithoutContext().Info("Server stopped")
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
s.tcpEntryPoints.Stop()
|
||||||
for epn, ep := range s.entryPointsTCP {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(entryPointName string, entryPoint *TCPEntryPoint) {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
entryPoint.Shutdown(ctx)
|
|
||||||
|
|
||||||
log.FromContext(ctx).Debugf("Entry point %s closed", entryPointName)
|
|
||||||
}(epn, ep)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
s.stopChan <- true
|
s.stopChan <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close destroys the server
|
// Close destroys the server
|
||||||
func (s *Server) Close() {
|
func (s *Server) Close() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
if ctx.Err() == context.Canceled {
|
if ctx.Err() == context.Canceled {
|
||||||
|
@ -226,125 +92,19 @@ func (s *Server) Close() {
|
||||||
}(ctx)
|
}(ctx)
|
||||||
|
|
||||||
stopMetricsClients()
|
stopMetricsClients()
|
||||||
|
|
||||||
s.routinesPool.Cleanup()
|
s.routinesPool.Cleanup()
|
||||||
close(s.configurationChan)
|
|
||||||
close(s.configurationValidatedChan)
|
|
||||||
signal.Stop(s.signals)
|
signal.Stop(s.signals)
|
||||||
close(s.signals)
|
close(s.signals)
|
||||||
|
|
||||||
close(s.stopChan)
|
close(s.stopChan)
|
||||||
|
|
||||||
if s.accessLoggerMiddleware != nil {
|
s.chainBuilder.Close()
|
||||||
if err := s.accessLoggerMiddleware.Close(); err != nil {
|
|
||||||
log.WithoutContext().Errorf("Could not close the access log file: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.tracer != nil {
|
|
||||||
s.tracer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) startTCPServers() {
|
|
||||||
// Use an empty configuration in order to initialize the default handlers with internal routes
|
|
||||||
routers := s.loadConfigurationTCP(dynamic.Configurations{})
|
|
||||||
for entryPointName, router := range routers {
|
|
||||||
s.entryPointsTCP[entryPointName].switchRouter(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
for entryPointName, serverEntryPoint := range s.entryPointsTCP {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
|
||||||
go serverEntryPoint.startTCP(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) listenProviders(stop chan bool) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case configMsg, ok := <-s.configurationChan:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if configMsg.Configuration != nil {
|
|
||||||
s.preLoadConfiguration(configMsg)
|
|
||||||
} else {
|
|
||||||
log.WithoutContext().WithField(log.ProviderName, configMsg.ProviderName).
|
|
||||||
Debug("Received nil configuration from provider, skipping.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddListener adds a new listener function used when new configuration is provided
|
|
||||||
func (s *Server) AddListener(listener func(dynamic.Configuration)) {
|
|
||||||
if s.configurationListeners == nil {
|
|
||||||
s.configurationListeners = make([]func(dynamic.Configuration), 0)
|
|
||||||
}
|
|
||||||
s.configurationListeners = append(s.configurationListeners, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) startProvider() {
|
|
||||||
logger := log.WithoutContext()
|
|
||||||
|
|
||||||
jsonConf, err := json.Marshal(s.provider)
|
|
||||||
if err != nil {
|
|
||||||
logger.Debugf("Unable to marshal provider configuration %T: %v", s.provider, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("Starting provider %T %s", s.provider, jsonConf)
|
|
||||||
currentProvider := s.provider
|
|
||||||
|
|
||||||
safe.Go(func() {
|
|
||||||
err := currentProvider.Provide(s.configurationChan, s.routinesPool)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Error starting provider %T: %s", s.provider, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerMetricClients(metricsConfig *types.Metrics) metrics.Registry {
|
|
||||||
if metricsConfig == nil {
|
|
||||||
return metrics.NewVoidRegistry()
|
|
||||||
}
|
|
||||||
|
|
||||||
var registries []metrics.Registry
|
|
||||||
|
|
||||||
if metricsConfig.Prometheus != nil {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "prometheus"))
|
|
||||||
prometheusRegister := metrics.RegisterPrometheus(ctx, metricsConfig.Prometheus)
|
|
||||||
if prometheusRegister != nil {
|
|
||||||
registries = append(registries, prometheusRegister)
|
|
||||||
log.FromContext(ctx).Debug("Configured Prometheus metrics")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if metricsConfig.Datadog != nil {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "datadog"))
|
|
||||||
registries = append(registries, metrics.RegisterDatadog(ctx, metricsConfig.Datadog))
|
|
||||||
log.FromContext(ctx).Debugf("Configured Datadog metrics: pushing to %s once every %s",
|
|
||||||
metricsConfig.Datadog.Address, metricsConfig.Datadog.PushInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
if metricsConfig.StatsD != nil {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "statsd"))
|
|
||||||
registries = append(registries, metrics.RegisterStatsd(ctx, metricsConfig.StatsD))
|
|
||||||
log.FromContext(ctx).Debugf("Configured StatsD metrics: pushing to %s once every %s",
|
|
||||||
metricsConfig.StatsD.Address, metricsConfig.StatsD.PushInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
if metricsConfig.InfluxDB != nil {
|
|
||||||
ctx := log.With(context.Background(), log.Str(log.MetricsProviderName, "influxdb"))
|
|
||||||
registries = append(registries, metrics.RegisterInfluxDB(ctx, metricsConfig.InfluxDB))
|
|
||||||
log.FromContext(ctx).Debugf("Configured InfluxDB metrics: pushing to %s once every %s",
|
|
||||||
metricsConfig.InfluxDB.Address, metricsConfig.InfluxDB.PushInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return metrics.NewMultiRegistry(registries)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopMetricsClients() {
|
func stopMetricsClients() {
|
||||||
metrics.StopDatadog()
|
metrics.StopDatadog()
|
||||||
metrics.StopStatsd()
|
metrics.StopStatsd()
|
||||||
|
|
|
@ -1,291 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/containous/alice"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
|
||||||
"github.com/containous/traefik/v2/pkg/metrics"
|
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
|
|
||||||
metricsmiddleware "github.com/containous/traefik/v2/pkg/middlewares/metrics"
|
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/requestdecorator"
|
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/tracing"
|
|
||||||
"github.com/containous/traefik/v2/pkg/responsemodifiers"
|
|
||||||
"github.com/containous/traefik/v2/pkg/server/middleware"
|
|
||||||
"github.com/containous/traefik/v2/pkg/server/router"
|
|
||||||
routertcp "github.com/containous/traefik/v2/pkg/server/router/tcp"
|
|
||||||
"github.com/containous/traefik/v2/pkg/server/service"
|
|
||||||
"github.com/containous/traefik/v2/pkg/server/service/tcp"
|
|
||||||
tcpCore "github.com/containous/traefik/v2/pkg/tcp"
|
|
||||||
"github.com/eapache/channels"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadConfiguration manages dynamically routers, middlewares, servers and TLS configurations
|
|
||||||
func (s *Server) loadConfiguration(configMsg dynamic.Message) {
|
|
||||||
currentConfigurations := s.currentConfigurations.Get().(dynamic.Configurations)
|
|
||||||
|
|
||||||
// Copy configurations to new map so we don't change current if LoadConfig fails
|
|
||||||
newConfigurations := currentConfigurations.DeepCopy()
|
|
||||||
newConfigurations[configMsg.ProviderName] = configMsg.Configuration
|
|
||||||
|
|
||||||
s.metricsRegistry.ConfigReloadsCounter().Add(1)
|
|
||||||
|
|
||||||
handlersTCP := s.loadConfigurationTCP(newConfigurations)
|
|
||||||
for entryPointName, router := range handlersTCP {
|
|
||||||
s.entryPointsTCP[entryPointName].switchRouter(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.metricsRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix()))
|
|
||||||
|
|
||||||
s.currentConfigurations.Set(newConfigurations)
|
|
||||||
|
|
||||||
for _, listener := range s.configurationListeners {
|
|
||||||
listener(*configMsg.Configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.metricsRegistry.IsEpEnabled() || s.metricsRegistry.IsSvcEnabled() {
|
|
||||||
var entrypoints []string
|
|
||||||
for key := range s.entryPointsTCP {
|
|
||||||
entrypoints = append(entrypoints, key)
|
|
||||||
}
|
|
||||||
metrics.OnConfigurationUpdate(newConfigurations, entrypoints)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadConfigurationTCP returns a new gorilla.mux Route from the specified global configuration and the dynamic
|
|
||||||
// provider configurations.
|
|
||||||
func (s *Server) loadConfigurationTCP(configurations dynamic.Configurations) map[string]*tcpCore.Router {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
var entryPoints []string
|
|
||||||
for entryPointName := range s.entryPointsTCP {
|
|
||||||
entryPoints = append(entryPoints, entryPointName)
|
|
||||||
}
|
|
||||||
|
|
||||||
conf := mergeConfiguration(configurations)
|
|
||||||
|
|
||||||
s.tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates)
|
|
||||||
|
|
||||||
rtConf := runtime.NewConfig(conf)
|
|
||||||
handlersNonTLS, handlersTLS := s.createHTTPHandlers(ctx, rtConf, entryPoints)
|
|
||||||
routersTCP := s.createTCPRouters(ctx, rtConf, entryPoints, handlersNonTLS, handlersTLS)
|
|
||||||
rtConf.PopulateUsedBy()
|
|
||||||
|
|
||||||
return routersTCP
|
|
||||||
}
|
|
||||||
|
|
||||||
// the given configuration must not be nil. its fields will get mutated.
|
|
||||||
func (s *Server) createTCPRouters(ctx context.Context, configuration *runtime.Configuration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler) map[string]*tcpCore.Router {
|
|
||||||
if configuration == nil {
|
|
||||||
return make(map[string]*tcpCore.Router)
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceManager := tcp.NewManager(configuration)
|
|
||||||
|
|
||||||
routerManager := routertcp.NewManager(configuration, serviceManager, handlers, handlersTLS, s.tlsManager)
|
|
||||||
|
|
||||||
return routerManager.BuildHandlers(ctx, entryPoints)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createHTTPHandlers returns, for the given configuration and entryPoints, the HTTP handlers for non-TLS connections, and for the TLS ones. the given configuration must not be nil. its fields will get mutated.
|
|
||||||
func (s *Server) createHTTPHandlers(ctx context.Context, configuration *runtime.Configuration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) {
|
|
||||||
var apiHandler http.Handler
|
|
||||||
if s.api != nil {
|
|
||||||
apiHandler = s.api(configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceManager := service.NewManager(configuration.Services, s.defaultRoundTripper, s.metricsRegistry, s.routinesPool, apiHandler, s.restHandler)
|
|
||||||
middlewaresBuilder := middleware.NewBuilder(configuration.Middlewares, serviceManager)
|
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(configuration.Middlewares)
|
|
||||||
routerManager := router.NewManager(configuration, serviceManager, middlewaresBuilder, responseModifierFactory)
|
|
||||||
|
|
||||||
handlersNonTLS := routerManager.BuildHandlers(ctx, entryPoints, false)
|
|
||||||
handlersTLS := routerManager.BuildHandlers(ctx, entryPoints, true)
|
|
||||||
|
|
||||||
routerHandlers := make(map[string]http.Handler)
|
|
||||||
for _, entryPointName := range entryPoints {
|
|
||||||
internalMuxRouter := mux.NewRouter().SkipClean(true)
|
|
||||||
|
|
||||||
ctx = log.With(ctx, log.Str(log.EntryPointName, entryPointName))
|
|
||||||
|
|
||||||
factory := s.entryPointsTCP[entryPointName].RouteAppenderFactory
|
|
||||||
if factory != nil {
|
|
||||||
// FIXME remove currentConfigurations
|
|
||||||
appender := factory.NewAppender(ctx, configuration)
|
|
||||||
appender.Append(internalMuxRouter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h, ok := handlersNonTLS[entryPointName]; ok {
|
|
||||||
internalMuxRouter.NotFoundHandler = h
|
|
||||||
} else {
|
|
||||||
internalMuxRouter.NotFoundHandler = buildDefaultHTTPRouter()
|
|
||||||
}
|
|
||||||
|
|
||||||
routerHandlers[entryPointName] = internalMuxRouter
|
|
||||||
|
|
||||||
chain := alice.New()
|
|
||||||
|
|
||||||
if s.accessLoggerMiddleware != nil {
|
|
||||||
chain = chain.Append(accesslog.WrapHandler(s.accessLoggerMiddleware))
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.tracer != nil {
|
|
||||||
chain = chain.Append(tracing.WrapEntryPointHandler(ctx, s.tracer, entryPointName))
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.metricsRegistry.IsEpEnabled() {
|
|
||||||
chain = chain.Append(metricsmiddleware.WrapEntryPointHandler(ctx, s.metricsRegistry, entryPointName))
|
|
||||||
}
|
|
||||||
|
|
||||||
chain = chain.Append(requestdecorator.WrapHandler(s.requestDecorator))
|
|
||||||
|
|
||||||
handler, err := chain.Then(internalMuxRouter.NotFoundHandler)
|
|
||||||
if err != nil {
|
|
||||||
log.FromContext(ctx).Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
internalMuxRouter.NotFoundHandler = handler
|
|
||||||
|
|
||||||
handlerTLS, ok := handlersTLS[entryPointName]
|
|
||||||
if ok {
|
|
||||||
handlerTLSWithMiddlewares, err := chain.Then(handlerTLS)
|
|
||||||
if err != nil {
|
|
||||||
log.FromContext(ctx).Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
handlersTLS[entryPointName] = handlerTLSWithMiddlewares
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return routerHandlers, handlersTLS
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmptyConfiguration(conf *dynamic.Configuration) bool {
|
|
||||||
if conf == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if conf.TCP == nil {
|
|
||||||
conf.TCP = &dynamic.TCPConfiguration{}
|
|
||||||
}
|
|
||||||
if conf.HTTP == nil {
|
|
||||||
conf.HTTP = &dynamic.HTTPConfiguration{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf.HTTP.Routers == nil &&
|
|
||||||
conf.HTTP.Services == nil &&
|
|
||||||
conf.HTTP.Middlewares == nil &&
|
|
||||||
(conf.TLS == nil || conf.TLS.Certificates == nil && conf.TLS.Stores == nil && conf.TLS.Options == nil) &&
|
|
||||||
conf.TCP.Routers == nil &&
|
|
||||||
conf.TCP.Services == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) preLoadConfiguration(configMsg dynamic.Message) {
|
|
||||||
s.defaultConfigurationValues(configMsg.Configuration.HTTP)
|
|
||||||
currentConfigurations := s.currentConfigurations.Get().(dynamic.Configurations)
|
|
||||||
|
|
||||||
logger := log.WithoutContext().WithField(log.ProviderName, configMsg.ProviderName)
|
|
||||||
if log.GetLevel() == logrus.DebugLevel {
|
|
||||||
copyConf := configMsg.Configuration.DeepCopy()
|
|
||||||
if copyConf.TLS != nil {
|
|
||||||
copyConf.TLS.Certificates = nil
|
|
||||||
|
|
||||||
for _, v := range copyConf.TLS.Stores {
|
|
||||||
v.DefaultCertificate = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonConf, err := json.Marshal(copyConf)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Could not marshal dynamic configuration: %v", err)
|
|
||||||
logger.Debugf("Configuration received from provider %s: [struct] %#v", configMsg.ProviderName, copyConf)
|
|
||||||
} else {
|
|
||||||
logger.Debugf("Configuration received from provider %s: %s", configMsg.ProviderName, string(jsonConf))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isEmptyConfiguration(configMsg.Configuration) {
|
|
||||||
logger.Infof("Skipping empty Configuration for provider %s", configMsg.ProviderName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
|
|
||||||
logger.Infof("Skipping same configuration for provider %s", configMsg.ProviderName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
providerConfigUpdateCh, ok := s.providerConfigUpdateMap[configMsg.ProviderName]
|
|
||||||
if !ok {
|
|
||||||
providerConfigUpdateCh = make(chan dynamic.Message)
|
|
||||||
s.providerConfigUpdateMap[configMsg.ProviderName] = providerConfigUpdateCh
|
|
||||||
s.routinesPool.Go(func(stop chan bool) {
|
|
||||||
s.throttleProviderConfigReload(s.providersThrottleDuration, s.configurationValidatedChan, providerConfigUpdateCh, stop)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
providerConfigUpdateCh <- configMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) defaultConfigurationValues(configuration *dynamic.HTTPConfiguration) {
|
|
||||||
// FIXME create a config hook
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) listenConfigurations(stop chan bool) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case configMsg, ok := <-s.configurationValidatedChan:
|
|
||||||
if !ok || configMsg.Configuration == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.loadConfiguration(configMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// throttleProviderConfigReload throttles the configuration reload speed for a single provider.
|
|
||||||
// It will immediately publish a new configuration and then only publish the next configuration after the throttle duration.
|
|
||||||
// Note that in the case it receives N new configs in the timeframe of the throttle duration after publishing,
|
|
||||||
// it will publish the last of the newly received configurations.
|
|
||||||
func (s *Server) throttleProviderConfigReload(throttle time.Duration, publish chan<- dynamic.Message, in <-chan dynamic.Message, stop chan bool) {
|
|
||||||
ring := channels.NewRingChannel(1)
|
|
||||||
defer ring.Close()
|
|
||||||
|
|
||||||
s.routinesPool.Go(func(stop chan bool) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case nextConfig := <-ring.Out():
|
|
||||||
if config, ok := nextConfig.(dynamic.Message); ok {
|
|
||||||
publish <- config
|
|
||||||
time.Sleep(throttle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case nextConfig := <-in:
|
|
||||||
ring.In() <- nextConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildDefaultHTTPRouter() *mux.Router {
|
|
||||||
rt := mux.NewRouter()
|
|
||||||
rt.NotFoundHandler = http.HandlerFunc(http.NotFound)
|
|
||||||
rt.SkipClean(true)
|
|
||||||
return rt
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
|
||||||
th "github.com/containous/traefik/v2/pkg/testhelpers"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReuseService(t *testing.T) {
|
|
||||||
testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
defer testServer.Close()
|
|
||||||
|
|
||||||
entryPoints := TCPEntryPoints{
|
|
||||||
"http": &TCPEntryPoint{},
|
|
||||||
}
|
|
||||||
|
|
||||||
staticConfig := static.Configuration{}
|
|
||||||
|
|
||||||
dynamicConfigs := th.BuildConfiguration(
|
|
||||||
th.WithRouters(
|
|
||||||
th.WithRouter("foo",
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRule("Path(`/ok`)")),
|
|
||||||
th.WithRouter("foo2",
|
|
||||||
th.WithEntryPoints("http"),
|
|
||||||
th.WithRule("Path(`/unauthorized`)"),
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRouterMiddlewares("basicauth")),
|
|
||||||
),
|
|
||||||
th.WithMiddlewares(th.WithMiddleware("basicauth",
|
|
||||||
th.WithBasicAuth(&dynamic.BasicAuth{Users: []string{"foo:bar"}}),
|
|
||||||
)),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar",
|
|
||||||
th.WithServers(th.WithServer(testServer.URL))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
srv := NewServer(staticConfig, nil, entryPoints, nil)
|
|
||||||
|
|
||||||
rtConf := runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs})
|
|
||||||
entrypointsHandlers, _ := srv.createHTTPHandlers(context.Background(), rtConf, []string{"http"})
|
|
||||||
|
|
||||||
// Test that the /ok path returns a status 200.
|
|
||||||
responseRecorderOk := &httptest.ResponseRecorder{}
|
|
||||||
requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/ok", nil)
|
|
||||||
entrypointsHandlers["http"].ServeHTTP(responseRecorderOk, requestOk)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code")
|
|
||||||
|
|
||||||
// Test that the /unauthorized path returns a 401 because of
|
|
||||||
// the basic authentication defined on the frontend.
|
|
||||||
responseRecorderUnauthorized := &httptest.ResponseRecorder{}
|
|
||||||
requestUnauthorized := httptest.NewRequest(http.MethodGet, testServer.URL+"/unauthorized", nil)
|
|
||||||
entrypointsHandlers["http"].ServeHTTP(responseRecorderUnauthorized, requestUnauthorized)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, responseRecorderUnauthorized.Result().StatusCode, "status code")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestThrottleProviderConfigReload(t *testing.T) {
|
|
||||||
throttleDuration := 30 * time.Millisecond
|
|
||||||
publishConfig := make(chan dynamic.Message)
|
|
||||||
providerConfig := make(chan dynamic.Message)
|
|
||||||
stop := make(chan bool)
|
|
||||||
defer func() {
|
|
||||||
stop <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
staticConfiguration := static.Configuration{}
|
|
||||||
server := NewServer(staticConfiguration, nil, nil, nil)
|
|
||||||
|
|
||||||
go server.throttleProviderConfigReload(throttleDuration, publishConfig, providerConfig, stop)
|
|
||||||
|
|
||||||
publishedConfigCount := 0
|
|
||||||
stopConsumeConfigs := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case <-stopConsumeConfigs:
|
|
||||||
return
|
|
||||||
case <-publishConfig:
|
|
||||||
publishedConfigCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// publish 5 new configs, one new config each 10 milliseconds
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
providerConfig <- dynamic.Message{}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// after 50 milliseconds 5 new configs were published
|
|
||||||
// with a throttle duration of 30 milliseconds this means, we should have received 2 new configs
|
|
||||||
assert.Equal(t, 2, publishedConfigCount, "times configs were published")
|
|
||||||
|
|
||||||
stopConsumeConfigs <- true
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-publishConfig:
|
|
||||||
// There should be exactly one more message that we receive after ~60 milliseconds since the start of the test.
|
|
||||||
select {
|
|
||||||
case <-publishConfig:
|
|
||||||
t.Error("extra config publication found")
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
t.Error("Last config was not published in time")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares"
|
"github.com/containous/traefik/v2/pkg/middlewares"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/forwardedheaders"
|
"github.com/containous/traefik/v2/pkg/middlewares/forwardedheaders"
|
||||||
"github.com/containous/traefik/v2/pkg/safe"
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/router"
|
||||||
"github.com/containous/traefik/v2/pkg/tcp"
|
"github.com/containous/traefik/v2/pkg/tcp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
@ -50,11 +51,60 @@ func (h *httpForwarder) Accept() (net.Conn, error) {
|
||||||
// TCPEntryPoints holds a map of TCPEntryPoint (the entrypoint names being the keys)
|
// TCPEntryPoints holds a map of TCPEntryPoint (the entrypoint names being the keys)
|
||||||
type TCPEntryPoints map[string]*TCPEntryPoint
|
type TCPEntryPoints map[string]*TCPEntryPoint
|
||||||
|
|
||||||
|
// NewTCPEntryPoints creates a new TCPEntryPoints.
|
||||||
|
func NewTCPEntryPoints(staticConfiguration static.Configuration) (TCPEntryPoints, error) {
|
||||||
|
serverEntryPointsTCP := make(TCPEntryPoints)
|
||||||
|
for entryPointName, config := range staticConfiguration.EntryPoints {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
serverEntryPointsTCP[entryPointName], err = NewTCPEntryPoint(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverEntryPointsTCP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server entry points.
|
||||||
|
func (eps TCPEntryPoints) Start() {
|
||||||
|
for entryPointName, serverEntryPoint := range eps {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
||||||
|
go serverEntryPoint.StartTCP(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the server entry points.
|
||||||
|
func (eps TCPEntryPoints) Stop() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for epn, ep := range eps {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(entryPointName string, entryPoint *TCPEntryPoint) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
||||||
|
entryPoint.Shutdown(ctx)
|
||||||
|
|
||||||
|
log.FromContext(ctx).Debugf("Entry point %s closed", entryPointName)
|
||||||
|
}(epn, ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch the TCP routers.
|
||||||
|
func (eps TCPEntryPoints) Switch(routersTCP map[string]*tcp.Router) {
|
||||||
|
for entryPointName, rt := range routersTCP {
|
||||||
|
eps[entryPointName].SwitchRouter(rt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TCPEntryPoint is the TCP server
|
// TCPEntryPoint is the TCP server
|
||||||
type TCPEntryPoint struct {
|
type TCPEntryPoint struct {
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
switcher *tcp.HandlerSwitcher
|
switcher *tcp.HandlerSwitcher
|
||||||
RouteAppenderFactory RouteAppenderFactory
|
|
||||||
transportConfiguration *static.EntryPointsTransport
|
transportConfiguration *static.EntryPointsTransport
|
||||||
tracker *connectionTracker
|
tracker *connectionTracker
|
||||||
httpServer *httpServer
|
httpServer *httpServer
|
||||||
|
@ -99,35 +149,8 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint) (*T
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeCloserWrapper wraps together a connection, and the concrete underlying
|
// StartTCP starts the TCP server.
|
||||||
// connection type that was found to satisfy WriteCloser.
|
func (e *TCPEntryPoint) StartTCP(ctx context.Context) {
|
||||||
type writeCloserWrapper struct {
|
|
||||||
net.Conn
|
|
||||||
writeCloser tcp.WriteCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *writeCloserWrapper) CloseWrite() error {
|
|
||||||
return c.writeCloser.CloseWrite()
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeCloser returns the given connection, augmented with the WriteCloser
|
|
||||||
// implementation, if any was found within the underlying conn.
|
|
||||||
func writeCloser(conn net.Conn) (tcp.WriteCloser, error) {
|
|
||||||
switch typedConn := conn.(type) {
|
|
||||||
case *proxyprotocol.Conn:
|
|
||||||
underlying, err := writeCloser(typedConn.Conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &writeCloserWrapper{writeCloser: underlying, Conn: typedConn}, nil
|
|
||||||
case *net.TCPConn:
|
|
||||||
return typedConn, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown connection type %T", typedConn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TCPEntryPoint) startTCP(ctx context.Context) {
|
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
logger.Debugf("Start TCP Server")
|
logger.Debugf("Start TCP Server")
|
||||||
|
|
||||||
|
@ -213,22 +236,55 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *TCPEntryPoint) switchRouter(router *tcp.Router) {
|
// SwitchRouter switches the TCP router handler.
|
||||||
router.HTTPForwarder(e.httpServer.Forwarder)
|
func (e *TCPEntryPoint) SwitchRouter(rt *tcp.Router) {
|
||||||
router.HTTPSForwarder(e.httpsServer.Forwarder)
|
rt.HTTPForwarder(e.httpServer.Forwarder)
|
||||||
|
|
||||||
httpHandler := router.GetHTTPHandler()
|
httpHandler := rt.GetHTTPHandler()
|
||||||
httpsHandler := router.GetHTTPSHandler()
|
|
||||||
if httpHandler == nil {
|
if httpHandler == nil {
|
||||||
httpHandler = buildDefaultHTTPRouter()
|
httpHandler = router.BuildDefaultHTTPRouter()
|
||||||
}
|
|
||||||
if httpsHandler == nil {
|
|
||||||
httpsHandler = buildDefaultHTTPRouter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.httpServer.Switcher.UpdateHandler(httpHandler)
|
e.httpServer.Switcher.UpdateHandler(httpHandler)
|
||||||
|
|
||||||
|
rt.HTTPSForwarder(e.httpsServer.Forwarder)
|
||||||
|
|
||||||
|
httpsHandler := rt.GetHTTPSHandler()
|
||||||
|
if httpsHandler == nil {
|
||||||
|
httpsHandler = router.BuildDefaultHTTPRouter()
|
||||||
|
}
|
||||||
|
|
||||||
e.httpsServer.Switcher.UpdateHandler(httpsHandler)
|
e.httpsServer.Switcher.UpdateHandler(httpsHandler)
|
||||||
e.switcher.Switch(router)
|
|
||||||
|
e.switcher.Switch(rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCloserWrapper wraps together a connection, and the concrete underlying
|
||||||
|
// connection type that was found to satisfy WriteCloser.
|
||||||
|
type writeCloserWrapper struct {
|
||||||
|
net.Conn
|
||||||
|
writeCloser tcp.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *writeCloserWrapper) CloseWrite() error {
|
||||||
|
return c.writeCloser.CloseWrite()
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCloser returns the given connection, augmented with the WriteCloser
|
||||||
|
// implementation, if any was found within the underlying conn.
|
||||||
|
func writeCloser(conn net.Conn) (tcp.WriteCloser, error) {
|
||||||
|
switch typedConn := conn.(type) {
|
||||||
|
case *proxyprotocol.Conn:
|
||||||
|
underlying, err := writeCloser(typedConn.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &writeCloserWrapper{writeCloser: underlying, Conn: typedConn}, nil
|
||||||
|
case *net.TCPConn:
|
||||||
|
return typedConn, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown connection type %T", typedConn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||||
|
@ -382,7 +438,7 @@ type httpServer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createHTTPServer(ctx context.Context, ln net.Listener, configuration *static.EntryPoint, withH2c bool) (*httpServer, error) {
|
func createHTTPServer(ctx context.Context, ln net.Listener, configuration *static.EntryPoint, withH2c bool) (*httpServer, error) {
|
||||||
httpSwitcher := middlewares.NewHandlerSwitcher(buildDefaultHTTPRouter())
|
httpSwitcher := middlewares.NewHandlerSwitcher(router.BuildDefaultHTTPRouter())
|
||||||
|
|
||||||
var handler http.Handler
|
var handler http.Handler
|
||||||
var err error
|
var err error
|
||||||
|
|
|
@ -28,14 +28,14 @@ func TestShutdownHTTP(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
go entryPoint.startTCP(context.Background())
|
go entryPoint.StartTCP(context.Background())
|
||||||
|
|
||||||
router := &tcp.Router{}
|
router := &tcp.Router{}
|
||||||
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
entryPoint.switchRouter(router)
|
entryPoint.SwitchRouter(router)
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -66,7 +66,7 @@ func TestShutdownHTTPHijacked(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
go entryPoint.startTCP(context.Background())
|
go entryPoint.StartTCP(context.Background())
|
||||||
|
|
||||||
router := &tcp.Router{}
|
router := &tcp.Router{}
|
||||||
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -79,7 +79,7 @@ func TestShutdownHTTPHijacked(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
entryPoint.switchRouter(router)
|
entryPoint.SwitchRouter(router)
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -110,7 +110,7 @@ func TestShutdownTCPConn(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
go entryPoint.startTCP(context.Background())
|
go entryPoint.StartTCP(context.Background())
|
||||||
|
|
||||||
router := &tcp.Router{}
|
router := &tcp.Router{}
|
||||||
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||||
|
@ -123,7 +123,7 @@ func TestShutdownTCPConn(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
entryPoint.switchRouter(router)
|
entryPoint.SwitchRouter(router)
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -1,268 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/static"
|
|
||||||
th "github.com/containous/traefik/v2/pkg/testhelpers"
|
|
||||||
"github.com/containous/traefik/v2/pkg/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestListenProvidersSkipsEmptyConfigs(t *testing.T) {
|
|
||||||
server, stop, invokeStopChan := setupListenProvider(10 * time.Millisecond)
|
|
||||||
defer invokeStopChan()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case <-server.configurationValidatedChan:
|
|
||||||
t.Error("An empty configuration was published but it should not")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
server.configurationChan <- dynamic.Message{ProviderName: "kubernetes"}
|
|
||||||
|
|
||||||
// give some time so that the configuration can be processed
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) {
|
|
||||||
server, stop, invokeStopChan := setupListenProvider(10 * time.Millisecond)
|
|
||||||
defer invokeStopChan()
|
|
||||||
|
|
||||||
publishedConfigCount := 0
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case conf := <-server.configurationValidatedChan:
|
|
||||||
// set the current configuration
|
|
||||||
// this is usually done in the processing part of the published configuration
|
|
||||||
// so we have to emulate the behavior here
|
|
||||||
currentConfigurations := server.currentConfigurations.Get().(dynamic.Configurations)
|
|
||||||
currentConfigurations[conf.ProviderName] = conf.Configuration
|
|
||||||
server.currentConfigurations.Set(currentConfigurations)
|
|
||||||
|
|
||||||
publishedConfigCount++
|
|
||||||
if publishedConfigCount > 1 {
|
|
||||||
t.Error("Same configuration should not be published multiple times")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
conf := &dynamic.Configuration{}
|
|
||||||
conf.HTTP = th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo")),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar")),
|
|
||||||
)
|
|
||||||
|
|
||||||
// provide a configuration
|
|
||||||
server.configurationChan <- dynamic.Message{ProviderName: "kubernetes", Configuration: conf}
|
|
||||||
|
|
||||||
// give some time so that the configuration can be processed
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
|
|
||||||
// provide the same configuration a second time
|
|
||||||
server.configurationChan <- dynamic.Message{ProviderName: "kubernetes", Configuration: conf}
|
|
||||||
|
|
||||||
// give some time so that the configuration can be processed
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
|
|
||||||
server, stop, invokeStopChan := setupListenProvider(10 * time.Millisecond)
|
|
||||||
defer invokeStopChan()
|
|
||||||
|
|
||||||
publishedProviderConfigCount := map[string]int{}
|
|
||||||
publishedConfigCount := 0
|
|
||||||
consumePublishedConfigsDone := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case newConfig := <-server.configurationValidatedChan:
|
|
||||||
publishedProviderConfigCount[newConfig.ProviderName]++
|
|
||||||
publishedConfigCount++
|
|
||||||
if publishedConfigCount == 2 {
|
|
||||||
consumePublishedConfigsDone <- true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conf := &dynamic.Configuration{}
|
|
||||||
conf.HTTP = th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo")),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar")),
|
|
||||||
)
|
|
||||||
server.configurationChan <- dynamic.Message{ProviderName: "kubernetes", Configuration: conf}
|
|
||||||
server.configurationChan <- dynamic.Message{ProviderName: "marathon", Configuration: conf}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-consumePublishedConfigsDone:
|
|
||||||
if val := publishedProviderConfigCount["kubernetes"]; val != 1 {
|
|
||||||
t.Errorf("Got %d configuration publication(s) for provider %q, want 1", val, "kubernetes")
|
|
||||||
}
|
|
||||||
if val := publishedProviderConfigCount["marathon"]; val != 1 {
|
|
||||||
t.Errorf("Got %d configuration publication(s) for provider %q, want 1", val, "marathon")
|
|
||||||
}
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
t.Errorf("Published configurations were not consumed in time")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupListenProvider configures the Server and starts listenProviders
|
|
||||||
func setupListenProvider(throttleDuration time.Duration) (server *Server, stop chan bool, invokeStopChan func()) {
|
|
||||||
stop = make(chan bool)
|
|
||||||
invokeStopChan = func() {
|
|
||||||
stop <- true
|
|
||||||
}
|
|
||||||
|
|
||||||
staticConfiguration := static.Configuration{
|
|
||||||
Providers: &static.Providers{
|
|
||||||
ProvidersThrottleDuration: types.Duration(throttleDuration),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
server = NewServer(staticConfiguration, nil, nil, nil)
|
|
||||||
go server.listenProviders(stop)
|
|
||||||
|
|
||||||
return server, stop, invokeStopChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerResponseEmptyBackend(t *testing.T) {
|
|
||||||
const requestPath = "/path"
|
|
||||||
const routeRule = "Path(`" + requestPath + "`)"
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
config func(testServerURL string) *dynamic.HTTPConfiguration
|
|
||||||
expectedStatusCode int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "Ok",
|
|
||||||
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
|
||||||
return th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo",
|
|
||||||
th.WithEntryPoints("http"),
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRule(routeRule)),
|
|
||||||
),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar",
|
|
||||||
th.WithServers(th.WithServer(testServerURL))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expectedStatusCode: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "No Frontend",
|
|
||||||
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
|
||||||
return th.BuildConfiguration()
|
|
||||||
},
|
|
||||||
expectedStatusCode: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Empty Backend LB",
|
|
||||||
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
|
||||||
return th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo",
|
|
||||||
th.WithEntryPoints("http"),
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRule(routeRule)),
|
|
||||||
),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar")),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expectedStatusCode: http.StatusServiceUnavailable,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Empty Backend LB Sticky",
|
|
||||||
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
|
||||||
return th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo",
|
|
||||||
th.WithEntryPoints("http"),
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRule(routeRule)),
|
|
||||||
),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar",
|
|
||||||
th.WithSticky("test")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expectedStatusCode: http.StatusServiceUnavailable,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Empty Backend LB",
|
|
||||||
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
|
||||||
return th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo",
|
|
||||||
th.WithEntryPoints("http"),
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRule(routeRule)),
|
|
||||||
),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar")),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expectedStatusCode: http.StatusServiceUnavailable,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Empty Backend LB Sticky",
|
|
||||||
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
|
||||||
return th.BuildConfiguration(
|
|
||||||
th.WithRouters(th.WithRouter("foo",
|
|
||||||
th.WithEntryPoints("http"),
|
|
||||||
th.WithServiceName("bar"),
|
|
||||||
th.WithRule(routeRule)),
|
|
||||||
),
|
|
||||||
th.WithLoadBalancerServices(th.WithService("bar",
|
|
||||||
th.WithSticky("test")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expectedStatusCode: http.StatusServiceUnavailable,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
test := test
|
|
||||||
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
defer testServer.Close()
|
|
||||||
|
|
||||||
globalConfig := static.Configuration{}
|
|
||||||
entryPointsConfig := TCPEntryPoints{
|
|
||||||
"http": &TCPEntryPoint{},
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := NewServer(globalConfig, nil, entryPointsConfig, nil)
|
|
||||||
rtConf := runtime.NewConfig(dynamic.Configuration{HTTP: test.config(testServer.URL)})
|
|
||||||
entryPoints, _ := srv.createHTTPHandlers(context.Background(), rtConf, []string{"http"})
|
|
||||||
|
|
||||||
responseRecorder := &httptest.ResponseRecorder{}
|
|
||||||
request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil)
|
|
||||||
|
|
||||||
entryPoints["http"].ServeHTTP(responseRecorder, request)
|
|
||||||
|
|
||||||
assert.Equal(t, test.expectedStatusCode, responseRecorder.Result().StatusCode, "status code")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
89
pkg/server/service/internalhandler.go
Normal file
89
pkg/server/service/internalhandler.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serviceManager interface {
|
||||||
|
BuildHTTP(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error)
|
||||||
|
LaunchHealthCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalHandlers is the internal HTTP handlers builder.
|
||||||
|
type InternalHandlers struct {
|
||||||
|
api http.Handler
|
||||||
|
dashboard http.Handler
|
||||||
|
rest http.Handler
|
||||||
|
prometheus http.Handler
|
||||||
|
ping http.Handler
|
||||||
|
serviceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInternalHandlers creates a new InternalHandlers.
|
||||||
|
func NewInternalHandlers(api func(configuration *runtime.Configuration) http.Handler, configuration *runtime.Configuration, rest http.Handler, metricsHandler http.Handler, pingHandler http.Handler, dashboard http.Handler, next serviceManager) *InternalHandlers {
|
||||||
|
var apiHandler http.Handler
|
||||||
|
if api != nil {
|
||||||
|
apiHandler = api(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InternalHandlers{
|
||||||
|
api: apiHandler,
|
||||||
|
dashboard: dashboard,
|
||||||
|
rest: rest,
|
||||||
|
prometheus: metricsHandler,
|
||||||
|
ping: pingHandler,
|
||||||
|
serviceManager: next,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildHTTP builds an HTTP handler.
|
||||||
|
func (m *InternalHandlers) BuildHTTP(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error) {
|
||||||
|
if strings.HasSuffix(serviceName, "@internal") {
|
||||||
|
return m.get(serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.serviceManager.BuildHTTP(rootCtx, serviceName, responseModifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InternalHandlers) get(serviceName string) (http.Handler, error) {
|
||||||
|
switch serviceName {
|
||||||
|
case "api@internal":
|
||||||
|
if m.api == nil {
|
||||||
|
return nil, errors.New("api is not enabled")
|
||||||
|
}
|
||||||
|
return m.api, nil
|
||||||
|
|
||||||
|
case "dashboard@internal":
|
||||||
|
if m.dashboard == nil {
|
||||||
|
return nil, errors.New("dashboard is not enabled")
|
||||||
|
}
|
||||||
|
return m.dashboard, nil
|
||||||
|
|
||||||
|
case "rest@internal":
|
||||||
|
if m.rest == nil {
|
||||||
|
return nil, errors.New("rest is not enabled")
|
||||||
|
}
|
||||||
|
return m.rest, nil
|
||||||
|
|
||||||
|
case "ping@internal":
|
||||||
|
if m.ping == nil {
|
||||||
|
return nil, errors.New("ping is not enabled")
|
||||||
|
}
|
||||||
|
return m.ping, nil
|
||||||
|
|
||||||
|
case "prometheus@internal":
|
||||||
|
if m.prometheus == nil {
|
||||||
|
return nil, errors.New("prometheus is not enabled")
|
||||||
|
}
|
||||||
|
return m.prometheus, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown internal service %s", serviceName)
|
||||||
|
}
|
||||||
|
}
|
61
pkg/server/service/managerfactory.go
Normal file
61
pkg/server/service/managerfactory.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/api"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/metrics"
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManagerFactory a factory of service manager.
|
||||||
|
type ManagerFactory struct {
|
||||||
|
metricsRegistry metrics.Registry
|
||||||
|
|
||||||
|
defaultRoundTripper http.RoundTripper
|
||||||
|
|
||||||
|
api func(configuration *runtime.Configuration) http.Handler
|
||||||
|
restHandler http.Handler
|
||||||
|
dashboardHandler http.Handler
|
||||||
|
metricsHandler http.Handler
|
||||||
|
pingHandler http.Handler
|
||||||
|
|
||||||
|
routinesPool *safe.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManagerFactory creates a new ManagerFactory.
|
||||||
|
func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, metricsRegistry metrics.Registry) *ManagerFactory {
|
||||||
|
factory := &ManagerFactory{
|
||||||
|
metricsRegistry: metricsRegistry,
|
||||||
|
defaultRoundTripper: setupDefaultRoundTripper(staticConfiguration.ServersTransport),
|
||||||
|
routinesPool: routinesPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
if staticConfiguration.API != nil {
|
||||||
|
factory.api = api.NewBuilder(staticConfiguration)
|
||||||
|
|
||||||
|
if staticConfiguration.API.Dashboard {
|
||||||
|
factory.dashboardHandler = http.FileServer(staticConfiguration.API.DashboardAssets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if staticConfiguration.Providers != nil && staticConfiguration.Providers.Rest != nil {
|
||||||
|
factory.restHandler = staticConfiguration.Providers.Rest.CreateRouter()
|
||||||
|
}
|
||||||
|
|
||||||
|
if staticConfiguration.Metrics != nil && staticConfiguration.Metrics.Prometheus != nil {
|
||||||
|
factory.metricsHandler = metrics.PrometheusHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
factory.pingHandler = staticConfiguration.Ping
|
||||||
|
|
||||||
|
return factory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build creates a service manager.
|
||||||
|
func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers {
|
||||||
|
svcManager := NewManager(configuration.Services, f.defaultRoundTripper, f.metricsRegistry, f.routinesPool)
|
||||||
|
return NewInternalHandlers(f.api, configuration, f.restHandler, f.metricsHandler, f.pingHandler, f.dashboardHandler, svcManager)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package server
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -98,3 +98,13 @@ func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool {
|
||||||
|
|
||||||
return roots
|
return roots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupDefaultRoundTripper(conf *static.ServersTransport) http.RoundTripper {
|
||||||
|
transport, err := createHTTPTransport(conf)
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Errorf("Could not configure HTTP Transport, fallbacking on default transport: %v", err)
|
||||||
|
return http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewManager creates a new Manager
|
// NewManager creates a new Manager
|
||||||
func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper http.RoundTripper, metricsRegistry metrics.Registry, routinePool *safe.Pool, api http.Handler, rest http.Handler) *Manager {
|
func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper http.RoundTripper, metricsRegistry metrics.Registry, routinePool *safe.Pool) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
routinePool: routinePool,
|
routinePool: routinePool,
|
||||||
metricsRegistry: metricsRegistry,
|
metricsRegistry: metricsRegistry,
|
||||||
|
@ -42,8 +42,6 @@ func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper htt
|
||||||
defaultRoundTripper: defaultRoundTripper,
|
defaultRoundTripper: defaultRoundTripper,
|
||||||
balancers: make(map[string][]healthcheck.BalancerHandler),
|
balancers: make(map[string][]healthcheck.BalancerHandler),
|
||||||
configs: configs,
|
configs: configs,
|
||||||
api: api,
|
|
||||||
rest: rest,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,26 +53,10 @@ type Manager struct {
|
||||||
defaultRoundTripper http.RoundTripper
|
defaultRoundTripper http.RoundTripper
|
||||||
balancers map[string][]healthcheck.BalancerHandler
|
balancers map[string][]healthcheck.BalancerHandler
|
||||||
configs map[string]*runtime.ServiceInfo
|
configs map[string]*runtime.ServiceInfo
|
||||||
api http.Handler
|
|
||||||
rest http.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildHTTP Creates a http.Handler for a service configuration.
|
// BuildHTTP Creates a http.Handler for a service configuration.
|
||||||
func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error) {
|
func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error) {
|
||||||
if serviceName == "api@internal" {
|
|
||||||
if m.api == nil {
|
|
||||||
return nil, errors.New("api is not enabled")
|
|
||||||
}
|
|
||||||
return m.api, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if serviceName == "rest@internal" {
|
|
||||||
if m.rest == nil {
|
|
||||||
return nil, errors.New("rest is not enabled")
|
|
||||||
}
|
|
||||||
return m.rest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := log.With(rootCtx, log.Str(log.ServiceName, serviceName))
|
ctx := log.With(rootCtx, log.Str(log.ServiceName, serviceName))
|
||||||
|
|
||||||
serviceName = internal.GetQualifiedName(ctx, serviceName)
|
serviceName = internal.GetQualifiedName(ctx, serviceName)
|
||||||
|
|
|
@ -80,7 +80,7 @@ func TestGetLoadBalancer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
||||||
sm := NewManager(nil, http.DefaultTransport, nil, nil, nil, nil)
|
sm := NewManager(nil, http.DefaultTransport, nil, nil)
|
||||||
|
|
||||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("X-From", "first")
|
w.Header().Set("X-From", "first")
|
||||||
|
@ -332,7 +332,7 @@ func TestManager_Build(t *testing.T) {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
manager := NewManager(test.configs, http.DefaultTransport, nil, nil, nil, nil)
|
manager := NewManager(test.configs, http.DefaultTransport, nil, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if len(test.providerName) > 0 {
|
if len(test.providerName) > 0 {
|
||||||
|
@ -346,14 +346,16 @@ func TestManager_Build(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultipleTypeOnBuildHTTP(t *testing.T) {
|
func TestMultipleTypeOnBuildHTTP(t *testing.T) {
|
||||||
manager := NewManager(map[string]*runtime.ServiceInfo{
|
services := map[string]*runtime.ServiceInfo{
|
||||||
"test@file": {
|
"test@file": {
|
||||||
Service: &dynamic.Service{
|
Service: &dynamic.Service{
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{},
|
LoadBalancer: &dynamic.ServersLoadBalancer{},
|
||||||
Weighted: &dynamic.WeightedRoundRobin{},
|
Weighted: &dynamic.WeightedRoundRobin{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, http.DefaultTransport, nil, nil, nil, nil)
|
}
|
||||||
|
|
||||||
|
manager := NewManager(services, http.DefaultTransport, nil, nil)
|
||||||
|
|
||||||
_, err := manager.BuildHTTP(context.Background(), "test@file", nil)
|
_, err := manager.BuildHTTP(context.Background(), "test@file", nil)
|
||||||
assert.Error(t, err, "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
|
assert.Error(t, err, "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
|
||||||
|
|
70
pkg/server/tcprouterfactory.go
Normal file
70
pkg/server/tcprouterfactory.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/responsemodifiers"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/middleware"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/router"
|
||||||
|
routertcp "github.com/containous/traefik/v2/pkg/server/router/tcp"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/service"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/service/tcp"
|
||||||
|
tcpCore "github.com/containous/traefik/v2/pkg/tcp"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TCPRouterFactory the factory of TCP routers.
|
||||||
|
type TCPRouterFactory struct {
|
||||||
|
entryPoints []string
|
||||||
|
|
||||||
|
managerFactory *service.ManagerFactory
|
||||||
|
|
||||||
|
chainBuilder *middleware.ChainBuilder
|
||||||
|
tlsManager *tls.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTCPRouterFactory creates a new TCPRouterFactory
|
||||||
|
func NewTCPRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, chainBuilder *middleware.ChainBuilder) *TCPRouterFactory {
|
||||||
|
var entryPoints []string
|
||||||
|
for name := range staticConfiguration.EntryPoints {
|
||||||
|
entryPoints = append(entryPoints, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TCPRouterFactory{
|
||||||
|
entryPoints: entryPoints,
|
||||||
|
managerFactory: managerFactory,
|
||||||
|
tlsManager: tlsManager,
|
||||||
|
chainBuilder: chainBuilder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTCPRouters creates new TCPRouters
|
||||||
|
func (f *TCPRouterFactory) CreateTCPRouters(conf dynamic.Configuration) map[string]*tcpCore.Router {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
rtConf := runtime.NewConfig(conf)
|
||||||
|
|
||||||
|
// HTTP
|
||||||
|
serviceManager := f.managerFactory.Build(rtConf)
|
||||||
|
|
||||||
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
|
|
||||||
|
routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, f.chainBuilder)
|
||||||
|
|
||||||
|
handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPoints, false)
|
||||||
|
handlersTLS := routerManager.BuildHandlers(ctx, f.entryPoints, true)
|
||||||
|
|
||||||
|
// TCP
|
||||||
|
svcTCPManager := tcp.NewManager(rtConf)
|
||||||
|
|
||||||
|
rtTCPManager := routertcp.NewManager(rtConf, svcTCPManager, handlersNonTLS, handlersTLS, f.tlsManager)
|
||||||
|
routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPoints)
|
||||||
|
|
||||||
|
rtConf.PopulateUsedBy()
|
||||||
|
|
||||||
|
return routersTCP
|
||||||
|
}
|
234
pkg/server/tcprouterfactory_test.go
Normal file
234
pkg/server/tcprouterfactory_test.go
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/metrics"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/middleware"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/service"
|
||||||
|
th "github.com/containous/traefik/v2/pkg/testhelpers"
|
||||||
|
"github.com/containous/traefik/v2/pkg/tls"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReuseService(t *testing.T) {
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
staticConfig := static.Configuration{
|
||||||
|
EntryPoints: map[string]*static.EntryPoint{
|
||||||
|
"http": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicConfigs := th.BuildConfiguration(
|
||||||
|
th.WithRouters(
|
||||||
|
th.WithRouter("foo",
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRule("Path(`/ok`)")),
|
||||||
|
th.WithRouter("foo2",
|
||||||
|
th.WithEntryPoints("http"),
|
||||||
|
th.WithRule("Path(`/unauthorized`)"),
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRouterMiddlewares("basicauth")),
|
||||||
|
),
|
||||||
|
th.WithMiddlewares(th.WithMiddleware("basicauth",
|
||||||
|
th.WithBasicAuth(&dynamic.BasicAuth{Users: []string{"foo:bar"}}),
|
||||||
|
)),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar",
|
||||||
|
th.WithServers(th.WithServer(testServer.URL))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry())
|
||||||
|
tlsManager := tls.NewManager()
|
||||||
|
|
||||||
|
factory := NewTCPRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil))
|
||||||
|
|
||||||
|
entryPointsHandlers := factory.CreateTCPRouters(dynamic.Configuration{HTTP: dynamicConfigs})
|
||||||
|
|
||||||
|
// Test that the /ok path returns a status 200.
|
||||||
|
responseRecorderOk := &httptest.ResponseRecorder{}
|
||||||
|
requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/ok", nil)
|
||||||
|
entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code")
|
||||||
|
|
||||||
|
// Test that the /unauthorized path returns a 401 because of
|
||||||
|
// the basic authentication defined on the frontend.
|
||||||
|
responseRecorderUnauthorized := &httptest.ResponseRecorder{}
|
||||||
|
requestUnauthorized := httptest.NewRequest(http.MethodGet, testServer.URL+"/unauthorized", nil)
|
||||||
|
entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorderUnauthorized, requestUnauthorized)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, responseRecorderUnauthorized.Result().StatusCode, "status code")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerResponseEmptyBackend(t *testing.T) {
|
||||||
|
const requestPath = "/path"
|
||||||
|
const routeRule = "Path(`" + requestPath + "`)"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
config func(testServerURL string) *dynamic.HTTPConfiguration
|
||||||
|
expectedStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Ok",
|
||||||
|
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
||||||
|
return th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo",
|
||||||
|
th.WithEntryPoints("http"),
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRule(routeRule)),
|
||||||
|
),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar",
|
||||||
|
th.WithServers(th.WithServer(testServerURL))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "No Frontend",
|
||||||
|
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
||||||
|
return th.BuildConfiguration()
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB",
|
||||||
|
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
||||||
|
return th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo",
|
||||||
|
th.WithEntryPoints("http"),
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRule(routeRule)),
|
||||||
|
),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar")),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB Sticky",
|
||||||
|
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
||||||
|
return th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo",
|
||||||
|
th.WithEntryPoints("http"),
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRule(routeRule)),
|
||||||
|
),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar",
|
||||||
|
th.WithSticky("test")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB",
|
||||||
|
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
||||||
|
return th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo",
|
||||||
|
th.WithEntryPoints("http"),
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRule(routeRule)),
|
||||||
|
),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar")),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty Backend LB Sticky",
|
||||||
|
config: func(testServerURL string) *dynamic.HTTPConfiguration {
|
||||||
|
return th.BuildConfiguration(
|
||||||
|
th.WithRouters(th.WithRouter("foo",
|
||||||
|
th.WithEntryPoints("http"),
|
||||||
|
th.WithServiceName("bar"),
|
||||||
|
th.WithRule(routeRule)),
|
||||||
|
),
|
||||||
|
th.WithLoadBalancerServices(th.WithService("bar",
|
||||||
|
th.WithSticky("test")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
staticConfig := static.Configuration{
|
||||||
|
EntryPoints: map[string]*static.EntryPoint{
|
||||||
|
"http": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry())
|
||||||
|
tlsManager := tls.NewManager()
|
||||||
|
|
||||||
|
factory := NewTCPRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil))
|
||||||
|
|
||||||
|
entryPointsHandlers := factory.CreateTCPRouters(dynamic.Configuration{HTTP: test.config(testServer.URL)})
|
||||||
|
|
||||||
|
responseRecorder := &httptest.ResponseRecorder{}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil)
|
||||||
|
|
||||||
|
entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorder, request)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expectedStatusCode, responseRecorder.Result().StatusCode, "status code")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalServices(t *testing.T) {
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
staticConfig := static.Configuration{
|
||||||
|
API: &static.API{},
|
||||||
|
EntryPoints: map[string]*static.EntryPoint{
|
||||||
|
"http": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicConfigs := th.BuildConfiguration(
|
||||||
|
th.WithRouters(
|
||||||
|
th.WithRouter("foo",
|
||||||
|
th.WithServiceName("api@internal"),
|
||||||
|
th.WithRule("PathPrefix(`/api`)")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry())
|
||||||
|
tlsManager := tls.NewManager()
|
||||||
|
|
||||||
|
factory := NewTCPRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil))
|
||||||
|
|
||||||
|
entryPointsHandlers := factory.CreateTCPRouters(dynamic.Configuration{HTTP: dynamicConfigs})
|
||||||
|
|
||||||
|
// Test that the /ok path returns a status 200.
|
||||||
|
responseRecorderOk := &httptest.ResponseRecorder{}
|
||||||
|
requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/api/rawdata", nil)
|
||||||
|
entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code")
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ type Router struct {
|
||||||
func (r *Router) ServeTCP(conn WriteCloser) {
|
func (r *Router) ServeTCP(conn WriteCloser) {
|
||||||
// FIXME -- Check if ProxyProtocol changes the first bytes of the request
|
// FIXME -- Check if ProxyProtocol changes the first bytes of the request
|
||||||
|
|
||||||
if r.catchAllNoTLS != nil && len(r.routingTable) == 0 && r.httpsHandler == nil {
|
if r.catchAllNoTLS != nil && len(r.routingTable) == 0 {
|
||||||
r.catchAllNoTLS.ServeTCP(conn)
|
r.catchAllNoTLS.ServeTCP(conn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
|
||||||
}
|
}
|
||||||
return "", false, ""
|
return "", false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordTypeHandshake = 0x16
|
const recordTypeHandshake = 0x16
|
||||||
if hdr[0] != recordTypeHandshake {
|
if hdr[0] != recordTypeHandshake {
|
||||||
// log.Errorf("Error not tls")
|
// log.Errorf("Error not tls")
|
||||||
|
@ -196,12 +197,14 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
|
||||||
log.Errorf("Error while Peeking hello: %s", err)
|
log.Errorf("Error while Peeking hello: %s", err)
|
||||||
return "", false, getPeeked(br)
|
return "", false, getPeeked(br)
|
||||||
}
|
}
|
||||||
|
|
||||||
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
|
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
|
||||||
helloBytes, err := br.Peek(recordHeaderLen + recLen)
|
helloBytes, err := br.Peek(recordHeaderLen + recLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error while Hello: %s", err)
|
log.Errorf("Error while Hello: %s", err)
|
||||||
return "", true, getPeeked(br)
|
return "", true, getPeeked(br)
|
||||||
}
|
}
|
||||||
|
|
||||||
sni := ""
|
sni := ""
|
||||||
server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
|
server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
|
||||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
@ -210,6 +213,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
_ = server.Handshake()
|
_ = server.Handshake()
|
||||||
|
|
||||||
return sni, true, getPeeked(br)
|
return sni, true, getPeeked(br)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,9 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultTLSOptions the default TLS options.
|
||||||
|
var DefaultTLSOptions = Options{}
|
||||||
|
|
||||||
// Manager is the TLS option/store/configuration factory
|
// Manager is the TLS option/store/configuration factory
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
storesConfig map[string]Store
|
storesConfig map[string]Store
|
||||||
|
@ -27,7 +30,12 @@ type Manager struct {
|
||||||
|
|
||||||
// NewManager creates a new Manager
|
// NewManager creates a new Manager
|
||||||
func NewManager() *Manager {
|
func NewManager() *Manager {
|
||||||
return &Manager{}
|
return &Manager{
|
||||||
|
stores: map[string]*CertificateStore{},
|
||||||
|
configs: map[string]Options{
|
||||||
|
"default": DefaultTLSOptions,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateConfigs updates the TLS* configuration options
|
// UpdateConfigs updates the TLS* configuration options
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Prometheus struct {
|
||||||
AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"`
|
AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"`
|
||||||
AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"`
|
AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"`
|
||||||
EntryPoint string `description:"EntryPoint" export:"true" json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"`
|
EntryPoint string `description:"EntryPoint" export:"true" json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"`
|
||||||
|
ManualRouting bool `description:"Manual routing" json:"manualRouting,omitempty" toml:"manualRouting,omitempty" yaml:"manualRouting,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults sets the default values.
|
// SetDefaults sets the default values.
|
||||||
|
|
|
@ -69,7 +69,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e
|
||||||
return nil, fmt.Errorf("failed to load TLS keypair: %v", err)
|
return nil, fmt.Errorf("failed to load TLS keypair: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("tls cert is a file, but tls key is not")
|
return nil, fmt.Errorf("TLS cert is a file, but tls key is not")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if errKeyIsFile != nil {
|
if errKeyIsFile != nil {
|
||||||
|
@ -83,11 +83,10 @@ func (clientTLS *ClientTLS) CreateTLSConfig(ctx context.Context) (*tls.Config, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TLSConfig := &tls.Config{
|
return &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
RootCAs: caPool,
|
RootCAs: caPool,
|
||||||
InsecureSkipVerify: clientTLS.InsecureSkipVerify,
|
InsecureSkipVerify: clientTLS.InsecureSkipVerify,
|
||||||
ClientAuth: clientAuth,
|
ClientAuth: clientAuth,
|
||||||
}
|
}, nil
|
||||||
return TLSConfig, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ module.exports = function (ctx) {
|
||||||
supportIE: false,
|
supportIE: false,
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
publicPath: process.env.APP_PUBLIC_PATH || '/dashboard',
|
publicPath: process.env.APP_PUBLIC_PATH || '',
|
||||||
env: process.env.APP_ENV === 'development'
|
env: process.env.APP_ENV === 'development'
|
||||||
? { // staging:
|
? { // staging:
|
||||||
APP_ENV: JSON.stringify(process.env.APP_ENV),
|
APP_ENV: JSON.stringify(process.env.APP_ENV),
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<q-scroll-area :thumb-style="appThumbStyle" style="height:100%;">
|
<q-scroll-area :thumb-style="appThumbStyle" style="height:100%;">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row items-start no-wrap">
|
<div class="row items-start no-wrap">
|
||||||
<div class="col">
|
<div class="col" v-if="data.type">
|
||||||
<div class="text-subtitle2">TYPE</div>
|
<div class="text-subtitle2">TYPE</div>
|
||||||
<q-chip
|
<q-chip
|
||||||
dense
|
dense
|
||||||
|
|
4
webui/src/statics/providers/internal.svg
Normal file
4
webui/src/statics/providers/internal.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
|
||||||
|
<circle cx="36" cy="36" r="36" fill="#dfdfdf"/>
|
||||||
|
<path d="m20.79 54.774c-4.1802-0.36038-7.3744-1.6213-9.6977-3.8282-2.3638-2.2454-3.378-5.1728-2.9777-8.5946 0.46062-3.9378 2.3319-6.7185 5.6126-8.3408 2.5286-1.2503 5.0511-1.7259 9.1591-1.7269 2.9187-0.0012 5.3644 0.23777 9.0946 0.88675 0.92236 0.16048 1.7209 0.29185 1.7746 0.29196 0.07803 0 0.1399-0.29356 0.30914-1.4672 0.30621-2.1235 0.36109-2.8562 0.27767-3.7069-0.0971-0.99016-0.22383-1.4618-0.59895-2.2291-0.96784-1.9798-3.2653-3.1911-6.9547-3.6667-1.0551-0.13602-3.4903-0.15505-4.5473-0.0356-1.7709 0.20021-2.7591 0.45532-4.963 1.2811-1.2894 0.48314-1.2953 0.48463-1.8096 0.45444-0.88418-0.05181-1.4878-0.45212-1.8749-1.2434-0.15944-0.32586-0.18748-0.46038-0.18748-0.89945 0-0.65913 0.17166-1.009 0.76939-1.5682 1.3404-1.2539 3.596-2.3217 5.8726-2.7802 1.4577-0.29355 2.1753-0.35527 4.0958-0.35232 1.9873 0.0052 3.258 0.11526 5.024 0.44365 2.9374 0.54614 5.1678 1.5088 6.8497 2.9563 0.49904 0.42948 1.2515 1.2748 1.574 1.7681 0.09855 0.15078 0.20008 0.27415 0.2256 0.27415 0.02554 0 0.18857-0.18117 0.36231-0.40254 2.5467-3.2452 6.8359-5.082 11.861-5.0794 3.5543 0 6.8581 0.901 9.3207 2.5369 1.9533 1.2976 3.5402 3.2965 4.1812 5.267 0.36807 1.1314 0.42403 1.5495 0.42742 3.1928 0.0021 1.0225-0.02612 1.7122-0.08315 2.0318-0.57634 3.2298-1.9492 5.4674-4.332 7.0607-1.917 1.2818-4.3245 2.0318-7.4732 2.328-1.585 0.14912-4.4841 0.13504-6.3622-0.03097-1.5939-0.14085-3.5511-0.3887-4.386-0.55536-0.69888-0.13953-3.0921-0.53913-3.1118-0.51957-0.0087 0.0088-0.12256 0.72447-0.25302 1.5908-0.20832 1.3833-0.23783 1.7285-0.2422 2.833-0.0047 1.123 0.01128 1.32 0.14678 1.8383 0.43569 1.6665 1.3958 2.7813 3.1378 3.6431 1.7136 0.8478 3.51 1.2147 6.2243 1.2712 1.2017 0.02528 1.8171 0.0077 2.5689-0.07192 1.8225-0.19325 2.653-0.40528 5.0744-1.2954 0.60308-0.22174 1.2186-0.42349 1.3677-0.44836 0.31967-0.0533 0.91586 0.05677 1.2633 0.23329 0.62718 0.31853 1.1541 1.168 1.1508 1.8552-0.0036 0.80285-0.27442 1.2891-1.0693 1.9215-1.9005 1.5121-4.3271 2.4394-7.2238 2.7606-1.0329 0.11455-3.3912 0.13142-4.5304 0.0325-3.208-0.27863-5.4352-0.82731-7.4706-1.8403-1.5882-0.79041-2.7999-1.771-3.705-2.9982-0.22238-0.30156-0.41127-0.55579-0.41977-0.56509-0.0085-0.0093-0.15631 0.16492-0.32849 0.38701-0.76108 0.98176-1.9249 2.037-3.0623 2.7765-1.642 1.0676-3.9992 1.8821-6.3844 2.206-0.71132 0.09655-3.0714 0.17621-3.6765 0.12405zm4.0958-4.5958c1.9474-0.43158 3.6827-1.4044 4.9426-2.7705 1.1492-1.2461 2.0917-3.1129 2.6105-5.1701 0.2924-1.1597 0.77845-4.3872 0.67071-4.4538-0.04252-0.02632-0.29939-0.06785-0.57075-0.09242-0.27137-0.02477-1.0449-0.11589-1.7189-0.20305-4.0037-0.51781-6.4599-0.719-7.8368-0.6418-2.6178 0.14664-3.6851 0.37518-5.2568 1.1258-0.80521 0.38453-1.361 0.80183-1.8007 1.352-0.63087 0.78938-1.0117 1.5738-1.2771 2.6308-0.19315 0.76915-0.21286 2.7831-0.03359 3.432 0.30199 1.093 0.66048 1.7117 1.4666 2.5308 1.1038 1.1216 2.5589 1.8699 4.3639 2.2441 0.92293 0.19134 0.89562 0.18948 2.4409 0.16621 1.1213-0.01703 1.5408-0.04851 1.9995-0.15006zm25.51-15.133c1.6286-0.1437 2.6301-0.39655 3.8605-0.97459 1.0233-0.48081 1.6181-0.9722 2.1809-1.8019 0.82864-1.2215 1.1513-2.3775 1.143-4.0951-0.0059-1.2242-0.10967-1.7334-0.53228-2.6123-0.94425-1.9636-3.1624-3.3701-6.0716-3.85-0.72595-0.11971-2.6744-0.09845-3.4185 0.03717-1.4315 0.26119-2.6416 0.73727-3.7088 1.4591-1.9717 1.3336-3.3325 3.3439-4.0902 6.0426-0.29764 1.06-0.37698 1.4669-0.64234 3.2941l-0.24478 1.6855 0.32789 0.03611c0.99033 0.10981 3.2235 0.38021 4.5204 0.54739 0.81593 0.10517 1.8173 0.2174 2.2253 0.24944 1.1794 0.09258 3.2939 0.08396 4.4505-0.01807z" fill="#37abc8"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
Loading…
Reference in a new issue