diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index b551e0926..542766495 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/json" - "fmt" stdlog "log" "net/http" "os" @@ -20,12 +19,17 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "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" "github.com/containous/traefik/v2/pkg/provider/acme" "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/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" + "github.com/containous/traefik/v2/pkg/types" "github.com/containous/traefik/v2/pkg/version" "github.com/coreos/go-systemd/daemon" assetfs "github.com/elazarl/go-bindata-assetfs" @@ -77,7 +81,7 @@ func runCmd(staticConfiguration *static.Configuration) error { http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment if err := roundrobin.SetDefaultWeight(0); err != nil { - log.WithoutContext().Errorf("Could not set roundrobin default weight: %v", err) + log.WithoutContext().Errorf("Could not set round robin default weight: %v", err) } staticConfiguration.SetEffectiveConfiguration() @@ -105,43 +109,11 @@ func runCmd(staticConfiguration *static.Configuration) error { stats(staticConfiguration) - providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers) - - 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 { - return fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err) - } - serverEntryPointsTCP[entryPointName].RouteAppenderFactory = router.NewRouteAppenderFactory(*staticConfiguration, entryPointName, acmeProviders) + svr, err := setupServer(staticConfiguration) + if err != nil { + return err } - 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()) if staticConfiguration.Ping != nil { @@ -168,7 +140,7 @@ func runCmd(staticConfiguration *static.Configuration) error { for range tick { resp, errHealthCheck := healthcheck.Do(*staticConfiguration) if resp != nil { - resp.Body.Close() + _ = resp.Body.Close() } if staticConfiguration.Ping == nil || errHealthCheck == nil { @@ -188,6 +160,94 @@ func runCmd(staticConfiguration *static.Configuration) error { 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 func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider { challengeStore := acme.NewLocalChallengeStore() @@ -222,6 +282,60 @@ func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.Pr 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) { // configure default log flags stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags) diff --git a/docs/content/observability/metrics/prometheus.md b/docs/content/observability/metrics/prometheus.md index fcb95bd1c..54e617fc5 100644 --- a/docs/content/observability/metrics/prometheus.md +++ b/docs/content/observability/metrics/prometheus.md @@ -116,3 +116,25 @@ metrics: --entryPoints.metrics.address=":8082" --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 +``` diff --git a/docs/content/operations/ping.md b/docs/content/operations/ping.md index b0640207b..3a5785d9e 100644 --- a/docs/content/operations/ping.md +++ b/docs/content/operations/ping.md @@ -58,3 +58,23 @@ ping: --entryPoints.ping.address=":8082" --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 +``` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index f0868656f..2e9dd6a0d 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -213,6 +213,9 @@ Buckets for latency metrics. (Default: ```0.100000, 0.300000, 1.200000, 5.000000 `--metrics.prometheus.entrypoint`: EntryPoint (Default: ```traefik```) +`--metrics.prometheus.manualrouting`: +Manual routing (Default: ```false```) + `--metrics.statsd`: StatsD metrics exporter type. (Default: ```false```) @@ -237,6 +240,9 @@ Enable ping. (Default: ```false```) `--ping.entrypoint`: EntryPoint (Default: ```traefik```) +`--ping.manualrouting`: +Manual routing (Default: ```false```) + `--providers.consulcatalog.cache`: Use local agent caching for catalog reads. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index b0a1ff489..a75866b08 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -213,6 +213,9 @@ Buckets for latency metrics. (Default: ```0.100000, 0.300000, 1.200000, 5.000000 `TRAEFIK_METRICS_PROMETHEUS_ENTRYPOINT`: EntryPoint (Default: ```traefik```) +`TRAEFIK_METRICS_PROMETHEUS_MANUALROUTING`: +Manual routing (Default: ```false```) + `TRAEFIK_METRICS_STATSD`: StatsD metrics exporter type. (Default: ```false```) @@ -237,6 +240,9 @@ Enable ping. (Default: ```false```) `TRAEFIK_PING_ENTRYPOINT`: EntryPoint (Default: ```traefik```) +`TRAEFIK_PING_MANUALROUTING`: +Manual routing (Default: ```false```) + `TRAEFIK_PROVIDERS_CONSULCATALOG_CACHE`: Use local agent caching for catalog reads. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index ae6311f4d..cad3042ce 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -141,6 +141,7 @@ addEntryPointsLabels = true addServicesLabels = true entryPoint = "foobar" + manualRouting = true [metrics.datadog] address = "foobar" pushInterval = "10s" @@ -165,6 +166,7 @@ [ping] entryPoint = "foobar" + manualRouting = true [log] level = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index b6e9324e5..acee93fe2 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -148,6 +148,7 @@ metrics: addEntryPointsLabels: true addServicesLabels: true entryPoint: foobar + manualRouting: true datadog: address: foobar pushInterval: 42 @@ -171,6 +172,7 @@ metrics: addServicesLabels: true ping: entryPoint: foobar + manualRouting: true log: level: foobar filePath: foobar diff --git a/integration/access_log_test.go b/integration/access_log_test.go index 0e0aa925d..61433a12a 100644 --- a/integration/access_log_test.go +++ b/integration/access_log_test.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "os" + "strconv" "strings" "time" @@ -546,8 +547,16 @@ func checkAccessLogExactValuesOutput(c *check.C, values []accessLogValue) int { func extractLines(c *check.C) []string { accessLog, err := ioutil.ReadFile(traefikTestAccessLogFile) c.Assert(err, checker.IsNil) + 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) { @@ -580,28 +589,31 @@ func CheckAccessLogFormat(c *check.C, line string, i int) { c.Assert(err, checker.IsNil) c.Assert(results, checker.HasLen, 14) c.Assert(results[accesslog.OriginStatus], checker.Matches, `^(-|\d{3})$`) - c.Assert(results[accesslog.RequestCount], checker.Equals, fmt.Sprintf("%d", i+1)) - c.Assert(results[accesslog.RouterName], checker.Matches, `"rt-.+@docker"`) - c.Assert(results[accesslog.ServiceURL], checker.HasPrefix, "\"http://") + count, _ := strconv.Atoi(results[accesslog.RequestCount]) + c.Assert(count, checker.GreaterOrEqualThan, i+1) + 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$`) } func checkAccessLogExactValues(c *check.C, line string, i int, v accessLogValue) { results, err := accesslog.ParseAccessLog(line) - // c.Assert(nil, checker.Equals, line) c.Assert(err, checker.IsNil) c.Assert(results, checker.HasLen, 14) if len(v.user) > 0 { c.Assert(results[accesslog.ClientUsername], checker.Equals, v.user) } 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.ServiceURL], checker.Matches, `^"?`+v.serviceURL+`.*$`) c.Assert(results[accesslog.Duration], checker.Matches, `^\d+ms$`) } func waitForTraefik(c *check.C, containerName string) { + time.Sleep(1 * time.Second) + // Wait for Traefik to turn ready. req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/rawdata", nil) c.Assert(err, checker.IsNil) diff --git a/integration/docker_compose_test.go b/integration/docker_compose_test.go index 8fd14405a..bcf84295c 100644 --- a/integration/docker_compose_test.go +++ b/integration/docker_compose_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "os" + "strings" "time" "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) c.Assert(err, checker.IsNil) - // check that we have only one router - c.Assert(rtconf.Routers, checker.HasLen, 1) + // check that we have only three routers (the one from this test + 2 unrelated internal ones) + 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 - c.Assert(services, checker.HasLen, 1) - for k, v := range services { - c.Assert(k, checker.Equals, composeService+"-integrationtest"+composeProject+"@docker") - c.Assert(v.LoadBalancer.Servers, checker.HasLen, serviceCount) + c.Assert(services, checker.HasLen, 3) + for name, service := range services { + if strings.HasSuffix(name, "@internal") { + 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. } } diff --git a/integration/log_rotation_test.go b/integration/log_rotation_test.go index d1be2974d..b5b872f16 100644 --- a/integration/log_rotation_test.go +++ b/integration/log_rotation_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "os" + "strings" "syscall" "time" @@ -155,10 +156,14 @@ func verifyLogLines(c *check.C, fileName string, countInit int, accessLog bool) line := rotatedLog.Text() if accessLog { if len(line) > 0 { - CheckAccessLogFormat(c, line, count) + if !strings.Contains(line, "/api/rawdata") { + CheckAccessLogFormat(c, line, count) + count++ + } } + } else { + count++ } - count++ } return count diff --git a/integration/rest_test.go b/integration/rest_test.go index 0b6fa731f..b46bd3f07 100644 --- a/integration/rest_test.go +++ b/integration/rest_test.go @@ -30,6 +30,10 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) { c.Assert(err, checker.IsNil) 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. err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) @@ -44,15 +48,15 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) { config: &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "router1": { + "routerHTTP": { EntryPoints: []string{"web"}, Middlewares: []string{}, - Service: "service1", + Service: "serviceHTTP", Rule: "PathPrefix(`/`)", }, }, Services: map[string]*dynamic.Service{ - "service1": { + "serviceHTTP": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -71,14 +75,14 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) { config: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "router1": { + "routerTCP": { EntryPoints: []string{"web"}, - Service: "service1", + Service: "serviceTCP", Rule: "HostSNI(`*`)", }, }, Services: map[string]*dynamic.TCPService{ - "service1": { + "serviceTCP": { LoadBalancer: &dynamic.TCPServersLoadBalancer{ Servers: []dynamic.TCPServer{ { @@ -95,17 +99,17 @@ func (s *RestSuite) TestSimpleConfigurationInsecure(c *check.C) { } for _, test := range testCase { - json, err := json.Marshal(test.config) + data, err := json.Marshal(test.config) 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) response, err := http.DefaultClient.Do(request) c.Assert(err, checker.IsNil) 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) err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK)) diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index b3e431820..2418c781a 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -1,5 +1,33 @@ { "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": { "entryPoints": [ "web" @@ -31,6 +59,29 @@ } }, "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": { "chain": { "middlewares": [ @@ -52,11 +103,23 @@ } }, "services": { + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, "default-test.route-6b204d94623b3df4370c@kubernetescrd": { "loadBalancer": { "servers": [ { - "url": "http://10.42.0.2:80" + "url": "http://10.42.0.3:80" }, { "url": "http://10.42.0.6:80" @@ -69,7 +132,7 @@ "default-test.route-6b204d94623b3df4370c@kubernetescrd" ], "serverStatus": { - "http://10.42.0.2:80": "UP", + "http://10.42.0.3:80": "UP", "http://10.42.0.6:80": "UP" } }, @@ -77,7 +140,7 @@ "loadBalancer": { "servers": [ { - "url": "http://10.42.0.2:80" + "url": "http://10.42.0.3:80" }, { "url": "http://10.42.0.6:80" @@ -90,7 +153,7 @@ "default-test2.route-23c7f4c450289ee29016@kubernetescrd" ], "serverStatus": { - "http://10.42.0.2:80": "UP", + "http://10.42.0.3:80": "UP", "http://10.42.0.6:80": "UP" } } @@ -118,7 +181,7 @@ "terminationDelay": 100, "servers": [ { - "address": "10.42.0.4:8080" + "address": "10.42.0.2:8080" }, { "address": "10.42.0.5:8080" diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 422bde9a6..23b825cc7 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -1,5 +1,33 @@ { "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": { "service": "default-whoami-http", "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": { + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, "default-whoami-http@kubernetes": { "loadBalancer": { "servers": [ @@ -37,7 +102,7 @@ "url": "http://10.42.0.2:80" }, { - "url": "http://10.42.0.4:80" + "url": "http://10.42.0.3:80" } ], "passHostHeader": true @@ -50,7 +115,7 @@ ], "serverStatus": { "http://10.42.0.2:80": "UP", - "http://10.42.0.4:80": "UP" + "http://10.42.0.3:80": "UP" } } } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 001cc83d5..1e2ef460d 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -27,12 +27,6 @@ func (g DashboardHandler) Append(router *mux.Router) { 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). PathPrefix("/dashboard/"). Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets))) diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 87f844c6c..d2251fcd8 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -44,23 +44,19 @@ type RunTimeRepresentation struct { // Handler serves the configuration and status of Traefik on API endpoints. type Handler struct { - dashboard bool - debug bool + dashboard 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 *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 func NewBuilder(staticConfig static.Configuration) func(*runtime.Configuration) http.Handler { return func(configuration *runtime.Configuration) http.Handler { - router := mux.NewRouter() - New(staticConfig, configuration).Append(router) - return router + return New(staticConfig, configuration).createRouter() } } @@ -73,8 +69,7 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration } return &Handler{ - dashboard: staticConfig.API.Dashboard, - // statistics: staticConfig.API.Statistics, + dashboard: staticConfig.API.Dashboard, dashboardAssets: staticConfig.API.DashboardAssets, runtimeConfiguration: rConfig, staticConfig: staticConfig, @@ -82,8 +77,10 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration } } -// Append add api routes on a router -func (h Handler) Append(router *mux.Router) { +// createRouter creates API routes and router. +func (h Handler) createRouter() *mux.Router { + router := mux.NewRouter() + if h.debug { 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/{serviceID}").HandlerFunc(h.getTCPService) - // FIXME stats - // health route - // router.Methods(http.MethodGet).Path("/health").HandlerFunc(p.getHealthHandler) - version.Handler{}.Append(router) if h.dashboard { DashboardHandler{Assets: h.dashboardAssets}.Append(router) } + + return router } func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) { diff --git a/pkg/api/handler_entrypoint_test.go b/pkg/api/handler_entrypoint_test.go index 9e796e59d..74bec5905 100644 --- a/pkg/api/handler_entrypoint_test.go +++ b/pkg/api/handler_entrypoint_test.go @@ -11,7 +11,6 @@ import ( "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/static" - "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -199,10 +198,7 @@ func TestHandler_EntryPoints(t *testing.T) { t.Parallel() handler := New(test.conf, &runtime.Configuration{}) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) + server := httptest.NewServer(handler.createRouter()) resp, err := http.DefaultClient.Get(server.URL + test.path) require.NoError(t, err) diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go index b27d80d83..0d114855a 100644 --- a/pkg/api/handler_http_test.go +++ b/pkg/api/handler_http_test.go @@ -13,7 +13,6 @@ import ( "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/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -813,10 +812,7 @@ func TestHandler_HTTP(t *testing.T) { rtConf.GetRoutersByEntryPoints(context.Background(), []string{"web"}, false) handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) + server := httptest.NewServer(handler.createRouter()) resp, err := http.DefaultClient.Get(server.URL + test.path) require.NoError(t, err) diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index 10ff7f693..d6e3629bf 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -19,7 +19,6 @@ import ( "github.com/containous/traefik/v2/pkg/provider/rest" "github.com/containous/traefik/v2/pkg/tracing/jaeger" "github.com/containous/traefik/v2/pkg/types" - "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -252,10 +251,7 @@ func TestHandler_Overview(t *testing.T) { t.Parallel() handler := New(test.confStatic, &test.confDyn) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) + server := httptest.NewServer(handler.createRouter()) resp, err := http.DefaultClient.Get(server.URL + test.path) require.NoError(t, err) diff --git a/pkg/api/handler_tcp_test.go b/pkg/api/handler_tcp_test.go index f033c0292..a7957cd25 100644 --- a/pkg/api/handler_tcp_test.go +++ b/pkg/api/handler_tcp_test.go @@ -11,7 +11,6 @@ import ( "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/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -520,10 +519,7 @@ func TestHandler_TCP(t *testing.T) { rtConf.GetTCPRoutersByEntryPoints(context.Background(), []string{"web"}) handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) + server := httptest.NewServer(handler.createRouter()) resp, err := http.DefaultClient.Get(server.URL + test.path) require.NoError(t, err) diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go index 972ebffa5..683e162f7 100644 --- a/pkg/api/handler_test.go +++ b/pkg/api/handler_test.go @@ -11,7 +11,6 @@ import ( "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/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -137,10 +136,7 @@ func TestHandler_RawData(t *testing.T) { rtConf.PopulateUsedBy() handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) + server := httptest.NewServer(handler.createRouter()) resp, err := http.DefaultClient.Get(server.URL + test.path) require.NoError(t, err) diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 9e1aff800..af3eb8e7a 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -177,9 +177,9 @@ func (c *Configuration) SetEffectiveConfiguration() { } if (c.API != nil && c.API.Insecure) || - (c.Ping != nil && c.Ping.EntryPoint == DefaultInternalEntryPointName) || - (c.Metrics != nil && c.Metrics.Prometheus != nil && c.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) || - (c.Providers.Rest != nil) { + (c.Ping != nil && !c.Ping.ManualRouting && c.Ping.EntryPoint == DefaultInternalEntryPointName) || + (c.Metrics != nil && c.Metrics.Prometheus != nil && !c.Metrics.Prometheus.ManualRouting && c.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) || + (c.Providers != nil && c.Providers.Rest != nil && c.Providers.Rest.Insecure) { if _, ok := c.EntryPoints[DefaultInternalEntryPointName]; !ok { ep := &EntryPoint{Address: ":8080"} ep.SetDefaults() diff --git a/pkg/metrics/prometheus.go b/pkg/metrics/prometheus.go index ffe236b94..6003da55b 100644 --- a/pkg/metrics/prometheus.go +++ b/pkg/metrics/prometheus.go @@ -2,7 +2,6 @@ package metrics import ( "context" - "fmt" "net/http" "sort" "strings" @@ -13,7 +12,6 @@ import ( "github.com/containous/traefik/v2/pkg/safe" "github.com/containous/traefik/v2/pkg/types" "github.com/go-kit/kit/metrics" - "github.com/gorilla/mux" stdprometheus "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -63,11 +61,8 @@ var promState = newPrometheusState() var promRegistry = stdprometheus.NewRegistry() // PrometheusHandler exposes Prometheus routes. -type PrometheusHandler struct{} - -// 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{})) +func PrometheusHandler() http.Handler { + return promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{}) } // RegisterPrometheus registers all Prometheus metrics. @@ -216,21 +211,22 @@ func registerPromState(ctx context.Context) bool { // OnConfigurationUpdate receives the current configuration from Traefik. // It then converts the configuration to the optimized package internal format // and sets it to the promState. -func OnConfigurationUpdate(dynConf dynamic.Configurations, entryPoints []string) { +func OnConfigurationUpdate(conf dynamic.Configuration, entryPoints []string) { dynamicConfig := newDynamicConfig() for _, value := range entryPoints { dynamicConfig.entryPoints[value] = true } - for key, config := range dynConf { - for name := range config.HTTP.Routers { - dynamicConfig.routers[fmt.Sprintf("%s@%s", name, key)] = true - } - for serviceName, service := range config.HTTP.Services { - dynamicConfig.services[fmt.Sprintf("%s@%s", serviceName, key)] = make(map[string]bool) + for name := range conf.HTTP.Routers { + dynamicConfig.routers[name] = true + } + + for serviceName, service := range conf.HTTP.Services { + dynamicConfig.services[serviceName] = make(map[string]bool) + if service.LoadBalancer != nil { for _, server := range service.LoadBalancer.Servers { - dynamicConfig.services[fmt.Sprintf("%s@%s", serviceName, key)][server.URL] = true + dynamicConfig.services[serviceName][server.URL] = true } } } diff --git a/pkg/metrics/prometheus_test.go b/pkg/metrics/prometheus_test.go index 13eb3e874..de66a421c 100644 --- a/pkg/metrics/prometheus_test.go +++ b/pkg/metrics/prometheus_test.go @@ -281,20 +281,19 @@ func TestPrometheusMetricRemoval(t *testing.T) { prometheusRegistry := RegisterPrometheus(context.Background(), &types.Prometheus{AddEntryPointsLabels: true, AddServicesLabels: true}) defer promRegistry.Unregister(promState) - configurations := make(dynamic.Configurations) - configurations["providerName"] = &dynamic.Configuration{ + conf := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters( - th.WithRouter("foo", + th.WithRouter("foo@providerName", th.WithServiceName("bar")), ), - th.WithLoadBalancerServices(th.WithService("bar", + th.WithLoadBalancerServices(th.WithService("bar@providerName", 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. // Those metrics should be part of the /metrics output on the first scrape but diff --git a/pkg/ping/ping.go b/pkg/ping/ping.go index e839aa6c3..8cb804398 100644 --- a/pkg/ping/ping.go +++ b/pkg/ping/ping.go @@ -4,14 +4,13 @@ import ( "context" "fmt" "net/http" - - "github.com/gorilla/mux" ) // Handler expose ping routes. type Handler struct { - EntryPoint string `description:"EntryPoint" export:"true" json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"` - terminating bool + 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 } // SetDefaults sets the default values. @@ -27,15 +26,11 @@ func (h *Handler) WithContext(ctx context.Context) { }() } -// Append adds ping routes on a router. -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 - if h.terminating { - statusCode = http.StatusServiceUnavailable - } - response.WriteHeader(statusCode) - fmt.Fprint(response, http.StatusText(statusCode)) - }) +func (h *Handler) ServeHTTP(response http.ResponseWriter, request *http.Request) { + statusCode := http.StatusOK + if h.terminating { + statusCode = http.StatusServiceUnavailable + } + response.WriteHeader(statusCode) + fmt.Fprint(response, http.StatusText(statusCode)) } diff --git a/pkg/provider/acme/challenge_http.go b/pkg/provider/acme/challenge_http.go index c224e3de5..0ad6563dd 100644 --- a/pkg/provider/acme/challenge_http.go +++ b/pkg/provider/acme/challenge_http.go @@ -35,8 +35,11 @@ func (c *challengeHTTP) Timeout() (timeout, interval time.Duration) { return 60 * time.Second, 5 * time.Second } -// Append adds routes on internal router -func (p *Provider) Append(router *mux.Router) { +// CreateHandler creates a HTTP handler to expose the token for the HTTP challenge. +func (p *Provider) CreateHandler(notFoundHandler http.Handler) http.Handler { + router := mux.NewRouter().SkipClean(true) + router.NotFoundHandler = notFoundHandler + router.Methods(http.MethodGet). Path(http01.ChallengePath("{token}")). 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) })) + + return router } func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte { diff --git a/pkg/provider/rest/rest.go b/pkg/provider/rest/rest.go index 5363c15a1..350228fde 100644 --- a/pkg/provider/rest/rest.go +++ b/pkg/provider/rest/rest.go @@ -3,7 +3,6 @@ package rest import ( "encoding/json" "fmt" - "io/ioutil" "net/http" "github.com/containous/traefik/v2/pkg/config/dynamic" @@ -23,8 +22,7 @@ type Provider struct { } // SetDefaults sets the default values. -func (p *Provider) SetDefaults() { -} +func (p *Provider) SetDefaults() {} var templatesRenderer = render.New(render.Options{Directory: "nowhere"}) @@ -33,40 +31,32 @@ func (p *Provider) Init() error { return nil } -// Handler creates an http.Handler for the Rest API -func (p *Provider) Handler() http.Handler { +// CreateRouter creates a router for the Rest API +func (p *Provider) CreateRouter() *mux.Router { router := mux.NewRouter() - p.Append(router) + router.Methods(http.MethodPut).Path("/api/providers/{provider}").Handler(p) return router } -// Append add rest provider routes on a router. -func (p *Provider) Append(systemRouter *mux.Router) { - systemRouter. - Methods(http.MethodPut). - Path("/api/providers/{provider}"). - HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - vars := mux.Vars(request) - if vars["provider"] != "rest" { - response.WriteHeader(http.StatusBadRequest) - fmt.Fprint(response, "Only 'rest' provider can be updated through the REST API") - return - } +func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + if vars["provider"] != "rest" { + http.Error(rw, "Only 'rest' provider can be updated through the REST API", http.StatusBadRequest) + return + } - configuration := new(dynamic.Configuration) - body, _ := ioutil.ReadAll(request.Body) + configuration := new(dynamic.Configuration) - if err := json.Unmarshal(body, configuration); err != nil { - log.WithoutContext().Errorf("Error parsing configuration %+v", err) - http.Error(response, fmt.Sprintf("%+v", err), http.StatusBadRequest) - return - } + if err := json.NewDecoder(req.Body).Decode(configuration); err != nil { + log.WithoutContext().Errorf("Error parsing configuration %+v", err) + http.Error(rw, fmt.Sprintf("%+v", err), http.StatusBadRequest) + return + } - p.configurationChan <- dynamic.Message{ProviderName: "rest", Configuration: configuration} - if err := templatesRenderer.JSON(response, http.StatusOK, configuration); err != nil { - log.WithoutContext().Error(err) - } - }) + p.configurationChan <- dynamic.Message{ProviderName: "rest", Configuration: configuration} + if err := templatesRenderer.JSON(rw, http.StatusOK, configuration); err != nil { + log.WithoutContext().Error(err) + } } // Provide allows the provider to provide configurations to traefik diff --git a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json new file mode 100644 index 000000000..e5374cc5e --- /dev/null +++ b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json @@ -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": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json new file mode 100644 index 000000000..65b389f81 --- /dev/null +++ b/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json @@ -0,0 +1,19 @@ +{ + "http": { + "routers": { + "api": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "priority": 9223372036854775806 + } + }, + "services": { + "api": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json new file mode 100644 index 000000000..4cafeae55 --- /dev/null +++ b/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json @@ -0,0 +1,10 @@ +{ + "http": { + "services": { + "api": {}, + "dashboard": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json b/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json new file mode 100644 index 000000000..ec900d1b3 --- /dev/null +++ b/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json @@ -0,0 +1,9 @@ +{ + "http": { + "services": { + "api": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/full_configuration.json b/pkg/provider/traefik/fixtures/full_configuration.json new file mode 100644 index 000000000..fe00ff139 --- /dev/null +++ b/pkg/provider/traefik/fixtures/full_configuration.json @@ -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": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/full_configuration_secure.json b/pkg/provider/traefik/fixtures/full_configuration_secure.json new file mode 100644 index 000000000..99e3f3429 --- /dev/null +++ b/pkg/provider/traefik/fixtures/full_configuration_secure.json @@ -0,0 +1,13 @@ +{ + "http": { + "services": { + "api": {}, + "dashboard": {}, + "ping": {}, + "prometheus": {}, + "rest": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/ping_custom.json b/pkg/provider/traefik/fixtures/ping_custom.json new file mode 100644 index 000000000..a378f761d --- /dev/null +++ b/pkg/provider/traefik/fixtures/ping_custom.json @@ -0,0 +1,9 @@ +{ + "http": { + "services": { + "ping": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/ping_simple.json b/pkg/provider/traefik/fixtures/ping_simple.json new file mode 100644 index 000000000..97131f7b3 --- /dev/null +++ b/pkg/provider/traefik/fixtures/ping_simple.json @@ -0,0 +1,19 @@ +{ + "http": { + "routers": { + "ping": { + "entryPoints": [ + "test" + ], + "service": "ping@internal", + "rule": "PathPrefix(`/ping`)", + "priority": 9223372036854775807 + } + }, + "services": { + "ping": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/prometheus_custom.json b/pkg/provider/traefik/fixtures/prometheus_custom.json new file mode 100644 index 000000000..06a63857e --- /dev/null +++ b/pkg/provider/traefik/fixtures/prometheus_custom.json @@ -0,0 +1,9 @@ +{ + "http": { + "services": { + "prometheus": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/prometheus_simple.json b/pkg/provider/traefik/fixtures/prometheus_simple.json new file mode 100644 index 000000000..8ed85e11f --- /dev/null +++ b/pkg/provider/traefik/fixtures/prometheus_simple.json @@ -0,0 +1,19 @@ +{ + "http": { + "routers": { + "prometheus": { + "entryPoints": [ + "test" + ], + "service": "prometheus@internal", + "rule": "PathPrefix(`/metrics`)", + "priority": 9223372036854775807 + } + }, + "services": { + "prometheus": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/rest_insecure.json b/pkg/provider/traefik/fixtures/rest_insecure.json new file mode 100644 index 000000000..dd9722583 --- /dev/null +++ b/pkg/provider/traefik/fixtures/rest_insecure.json @@ -0,0 +1,19 @@ +{ + "http": { + "routers": { + "rest": { + "entryPoints": [ + "traefik" + ], + "service": "rest@internal", + "rule": "PathPrefix(`/api/providers`)", + "priority": 9223372036854775807 + } + }, + "services": { + "rest": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/rest_secure.json b/pkg/provider/traefik/fixtures/rest_secure.json new file mode 100644 index 000000000..1085b5719 --- /dev/null +++ b/pkg/provider/traefik/fixtures/rest_secure.json @@ -0,0 +1,9 @@ +{ + "http": { + "services": { + "rest": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go new file mode 100644 index 000000000..a4eebbebd --- /dev/null +++ b/pkg/provider/traefik/internal.go @@ -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{} +} diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go new file mode 100644 index 000000000..9d1edbc5e --- /dev/null +++ b/pkg/provider/traefik/internal_test.go @@ -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)) + }) + } +} diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 7ae5d6c2e..8f5a8d4a2 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -67,7 +67,7 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura } if len(defaultTLSOptionProviders) == 0 { - conf.TLS.Options["default"] = tls.Options{} + conf.TLS.Options["default"] = tls.DefaultTLSOptions } else if len(defaultTLSOptionProviders) > 1 { 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, diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go new file mode 100644 index 000000000..e593ae9cd --- /dev/null +++ b/pkg/server/configurationwatcher.go @@ -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 +} diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go new file mode 100644 index 000000000..1e502c615 --- /dev/null +++ b/pkg/server/configurationwatcher_test.go @@ -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) +} diff --git a/pkg/server/internal/provider_test.go b/pkg/server/internal/provider_test.go new file mode 100644 index 000000000..e6849e9f7 --- /dev/null +++ b/pkg/server/internal/provider_test.go @@ -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) + }) + } +} diff --git a/pkg/server/middleware/chainbuilder.go b/pkg/server/middleware/chainbuilder.go new file mode 100644 index 000000000..f8c6b390e --- /dev/null +++ b/pkg/server/middleware/chainbuilder.go @@ -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 +} diff --git a/pkg/server/router/route_appender_aggregator.go b/pkg/server/router/route_appender_aggregator.go deleted file mode 100644 index c6f50545d..000000000 --- a/pkg/server/router/route_appender_aggregator.go +++ /dev/null @@ -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 - } -} diff --git a/pkg/server/router/route_appender_aggregator_test.go b/pkg/server/router/route_appender_aggregator_test.go deleted file mode 100644 index bfa27ae58..000000000 --- a/pkg/server/router/route_appender_aggregator_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/pkg/server/router/route_appender_factory.go b/pkg/server/router/route_appender_factory.go deleted file mode 100644 index 21be5d4c9..000000000 --- a/pkg/server/router/route_appender_factory.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index f75a75e49..ed7fdee85 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -11,41 +11,55 @@ import ( "github.com/containous/traefik/v2/pkg/middlewares/accesslog" "github.com/containous/traefik/v2/pkg/middlewares/recovery" "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/server/internal" "github.com/containous/traefik/v2/pkg/server/middleware" - "github.com/containous/traefik/v2/pkg/server/service" ) const ( 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 func NewManager(conf *runtime.Configuration, - serviceManager *service.Manager, - middlewaresBuilder *middleware.Builder, - modifierBuilder *responsemodifiers.Builder, + serviceManager serviceManager, + middlewaresBuilder middlewareBuilder, + modifierBuilder responseModifierBuilder, + chainBuilder *middleware.ChainBuilder, ) *Manager { return &Manager{ routerHandlers: make(map[string]http.Handler), serviceManager: serviceManager, middlewaresBuilder: middlewaresBuilder, modifierBuilder: modifierBuilder, + chainBuilder: chainBuilder, 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 { if m.conf != nil { 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() 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) } + +// BuildDefaultHTTPRouter creates a default HTTP router. +func BuildDefaultHTTPRouter() http.Handler { + return http.NotFoundHandler() +} diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 654bde6d0..5b30e3395 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -10,6 +10,7 @@ import ( "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/middlewares/accesslog" "github.com/containous/traefik/v2/pkg/middlewares/requestdecorator" "github.com/containous/traefik/v2/pkg/responsemodifiers" @@ -24,7 +25,7 @@ import ( func TestRouterManager_Get(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - type ExpectedResult struct { + type expectedResult struct { StatusCode int RequestHeaders map[string]string } @@ -35,7 +36,7 @@ func TestRouterManager_Get(t *testing.T) { serviceConfig map[string]*dynamic.Service middlewaresConfig map[string]*dynamic.Middleware entryPoints []string - expected ExpectedResult + expected expectedResult }{ { desc: "no middleware", @@ -58,7 +59,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{StatusCode: http.StatusOK}, + expected: expectedResult{StatusCode: http.StatusOK}, }, { desc: "no load balancer", @@ -73,7 +74,7 @@ func TestRouterManager_Get(t *testing.T) { "foo-service": {}, }, entryPoints: []string{"web"}, - expected: ExpectedResult{StatusCode: http.StatusNotFound}, + expected: expectedResult{StatusCode: http.StatusNotFound}, }, { desc: "no middleware, default entry point", @@ -95,7 +96,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{StatusCode: http.StatusOK}, + expected: expectedResult{StatusCode: http.StatusOK}, }, { desc: "no middleware, no matching", @@ -118,7 +119,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{StatusCode: http.StatusNotFound}, + expected: expectedResult{StatusCode: http.StatusNotFound}, }, { desc: "middleware: headers > auth", @@ -154,7 +155,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{ + expected: expectedResult{ StatusCode: http.StatusUnauthorized, RequestHeaders: map[string]string{ "X-Apero": "beer", @@ -195,7 +196,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{ + expected: expectedResult{ StatusCode: http.StatusUnauthorized, RequestHeaders: map[string]string{ "X-Apero": "", @@ -223,7 +224,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{StatusCode: http.StatusOK}, + expected: expectedResult{StatusCode: http.StatusOK}, }, { desc: "no middleware with specified provider name", @@ -246,7 +247,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{StatusCode: http.StatusOK}, + expected: expectedResult{StatusCode: http.StatusOK}, }, { desc: "middleware: chain with provider name", @@ -285,7 +286,7 @@ func TestRouterManager_Get(t *testing.T) { }, }, entryPoints: []string{"web"}, - expected: ExpectedResult{ + expected: expectedResult{ StatusCode: http.StatusUnauthorized, RequestHeaders: map[string]string{ "X-Apero": "", @@ -306,10 +307,13 @@ func TestRouterManager_Get(t *testing.T) { 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) 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) @@ -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) 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) @@ -680,7 +686,6 @@ func TestRuntimeConfiguration(t *testing.T) { for _, test := range testCases { test := test - t.Run(test.desc, func(t *testing.T) { t.Parallel() @@ -693,10 +698,13 @@ func TestRuntimeConfiguration(t *testing.T) { 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) 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) @@ -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) 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) @@ -790,6 +801,7 @@ func BenchmarkRouterServe(b *testing.B) { StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader("")), } + routersConfig := map[string]*dynamic.Router{ "foo": { EntryPoints: []string{"web"}, @@ -817,10 +829,13 @@ func BenchmarkRouterServe(b *testing.B) { 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) 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) @@ -857,7 +872,8 @@ func BenchmarkService(b *testing.B) { 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() req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 55b42ba9d..1fdfb06fd 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -16,6 +16,11 @@ import ( traefiktls "github.com/containous/traefik/v2/pkg/tls" ) +const ( + defaultTLSConfigName = "default" + defaultTLSStoreName = "default" +) + // NewManager Creates a new Manager func NewManager(conf *runtime.Configuration, 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) { router := &tcp.Router{} router.HTTPHandler(handlerHTTP) - const defaultTLSConfigName = "default" - defaultTLSConf, err := m.tlsManager.Get("default", defaultTLSConfigName) + defaultTLSConf, err := m.tlsManager.Get(defaultTLSStoreName, defaultTLSConfigName) if err != nil { log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err) } router.HTTPSHandler(handlerHTTPS, defaultTLSConf) + if len(configsHTTP) > 0 { + router.AddRouteHTTPTLS("*", defaultTLSConf) + } + // Keyed by domain, then by options reference. tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} 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) } - tlsConf, err := m.tlsManager.Get("default", tlsOptionsName) + tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName) if err != nil { routerHTTPConfig.AddError(err, true) logger.Debug(err) continue } + if tlsOptionsForHostSNI[domain] == nil { tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig) } @@ -153,7 +162,9 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string config = v.TLSConfig break } + logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName) + router.AddRouteHTTPTLS(hostSNI, config) } else { 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) 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) + router.AddRouteHTTPTLS(hostSNI, defaultTLSConf) } } @@ -216,7 +229,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string tlsOptionsName = internal.GetQualifiedName(ctxRouter, tlsOptionsName) } - tlsConf, err := m.tlsManager.Get("default", tlsOptionsName) + tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName) if err != nil { routerConfig.AddError(err, true) logger.Debug(err) diff --git a/pkg/server/server.go b/pkg/server/server.go index b7bc15817..90fb8e82b 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,166 +2,47 @@ package server import ( "context" - "encoding/json" - "net/http" "os" "os/signal" - "sync" "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/metrics" "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/tls" - "github.com/containous/traefik/v2/pkg/tracing" - "github.com/containous/traefik/v2/pkg/tracing/jaeger" - "github.com/containous/traefik/v2/pkg/types" + "github.com/containous/traefik/v2/pkg/server/middleware" ) // Server is the reverse-proxy/load-balancer engine type Server struct { - entryPointsTCP TCPEntryPoints - configurationChan chan dynamic.Message - configurationValidatedChan chan dynamic.Message - signals chan os.Signal - stopChan chan bool - currentConfigurations safe.Safe - providerConfigUpdateMap map[string]chan dynamic.Message - accessLoggerMiddleware *accesslog.Handler - tracer *tracing.Tracing - 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 -} + watcher *ConfigurationWatcher + tcpEntryPoints TCPEntryPoints + chainBuilder *middleware.ChainBuilder -// RouteAppenderFactory the route appender factory interface -type RouteAppenderFactory interface { - NewAppender(ctx context.Context, runtimeConfiguration *runtime.Configuration) types.RouteAppender -} + accessLoggerMiddleware *accesslog.Handler -func setupTracing(conf *static.Tracing) tracing.Backend { - var backend tracing.Backend + signals chan os.Signal + stopChan chan bool - 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 + routinesPool *safe.Pool } // NewServer returns an initialized Server. -func NewServer(staticConfiguration static.Configuration, provider provider.Provider, entryPoints TCPEntryPoints, tlsManager *tls.Manager) *Server { - server := &Server{} - - if staticConfiguration.API != nil { - server.api = api.NewBuilder(staticConfiguration) +func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, watcher *ConfigurationWatcher, + chainBuilder *middleware.ChainBuilder, accessLoggerMiddleware *accesslog.Handler) *Server { + srv := &Server{ + watcher: watcher, + 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 { - server.restHandler = staticConfiguration.Providers.Rest.Handler() - } + srv.configureSignals() - server.provider = provider - 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 + return srv } // 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.startTCPServers() - s.routinesPool.Go(func(stop chan bool) { - s.listenProviders(stop) - }) - s.routinesPool.Go(func(stop chan bool) { - s.listenConfigurations(stop) - }) - s.startProvider() + s.tcpEntryPoints.Start() + s.watcher.Start() + s.routinesPool.Go(func(stop chan bool) { s.listenSignals(stop) }) } -// Wait blocks until server is shutted down. +// Wait blocks until the server shutdown. func (s *Server) Wait() { <-s.stopChan } @@ -197,25 +73,15 @@ func (s *Server) Wait() { func (s *Server) Stop() { defer log.WithoutContext().Info("Server stopped") - var wg sync.WaitGroup - 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() + s.tcpEntryPoints.Stop() - entryPoint.Shutdown(ctx) - - log.FromContext(ctx).Debugf("Entry point %s closed", entryPointName) - }(epn, ep) - } - wg.Wait() s.stopChan <- true } // Close destroys the server func (s *Server) Close() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + go func(ctx context.Context) { <-ctx.Done() if ctx.Err() == context.Canceled { @@ -226,125 +92,19 @@ func (s *Server) Close() { }(ctx) stopMetricsClients() + s.routinesPool.Cleanup() - close(s.configurationChan) - close(s.configurationValidatedChan) + signal.Stop(s.signals) close(s.signals) + close(s.stopChan) - if s.accessLoggerMiddleware != nil { - 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() - } + s.chainBuilder.Close() 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() { metrics.StopDatadog() metrics.StopStatsd() diff --git a/pkg/server/server_configuration.go b/pkg/server/server_configuration.go deleted file mode 100644 index edeb0ede4..000000000 --- a/pkg/server/server_configuration.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/server_configuration_test.go b/pkg/server/server_configuration_test.go deleted file mode 100644 index 732529f8e..000000000 --- a/pkg/server/server_configuration_test.go +++ /dev/null @@ -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") - } -} diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 51043403d..cac8d0aa4 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -16,6 +16,7 @@ import ( "github.com/containous/traefik/v2/pkg/middlewares" "github.com/containous/traefik/v2/pkg/middlewares/forwardedheaders" "github.com/containous/traefik/v2/pkg/safe" + "github.com/containous/traefik/v2/pkg/server/router" "github.com/containous/traefik/v2/pkg/tcp" "github.com/sirupsen/logrus" "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) 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 type TCPEntryPoint struct { listener net.Listener switcher *tcp.HandlerSwitcher - RouteAppenderFactory RouteAppenderFactory transportConfiguration *static.EntryPointsTransport tracker *connectionTracker httpServer *httpServer @@ -99,35 +149,8 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint) (*T }, nil } -// 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) - } -} - -func (e *TCPEntryPoint) startTCP(ctx context.Context) { +// StartTCP starts the TCP server. +func (e *TCPEntryPoint) StartTCP(ctx context.Context) { logger := log.FromContext(ctx) logger.Debugf("Start TCP Server") @@ -213,22 +236,55 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) { cancel() } -func (e *TCPEntryPoint) switchRouter(router *tcp.Router) { - router.HTTPForwarder(e.httpServer.Forwarder) - router.HTTPSForwarder(e.httpsServer.Forwarder) +// SwitchRouter switches the TCP router handler. +func (e *TCPEntryPoint) SwitchRouter(rt *tcp.Router) { + rt.HTTPForwarder(e.httpServer.Forwarder) - httpHandler := router.GetHTTPHandler() - httpsHandler := router.GetHTTPSHandler() + httpHandler := rt.GetHTTPHandler() if httpHandler == nil { - httpHandler = buildDefaultHTTPRouter() - } - if httpsHandler == nil { - httpsHandler = buildDefaultHTTPRouter() + httpHandler = router.BuildDefaultHTTPRouter() } 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.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 @@ -382,7 +438,7 @@ type httpServer struct { } 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 err error diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 17ccd81a1..425a06390 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -28,14 +28,14 @@ func TestShutdownHTTP(t *testing.T) { }) require.NoError(t, err) - go entryPoint.startTCP(context.Background()) + go entryPoint.StartTCP(context.Background()) router := &tcp.Router{} router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { time.Sleep(1 * time.Second) rw.WriteHeader(http.StatusOK) })) - entryPoint.switchRouter(router) + entryPoint.SwitchRouter(router) conn, err := net.Dial("tcp", entryPoint.listener.Addr().String()) require.NoError(t, err) @@ -66,7 +66,7 @@ func TestShutdownHTTPHijacked(t *testing.T) { }) require.NoError(t, err) - go entryPoint.startTCP(context.Background()) + go entryPoint.StartTCP(context.Background()) router := &tcp.Router{} router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { @@ -79,7 +79,7 @@ func TestShutdownHTTPHijacked(t *testing.T) { require.NoError(t, err) })) - entryPoint.switchRouter(router) + entryPoint.SwitchRouter(router) conn, err := net.Dial("tcp", entryPoint.listener.Addr().String()) require.NoError(t, err) @@ -110,7 +110,7 @@ func TestShutdownTCPConn(t *testing.T) { }) require.NoError(t, err) - go entryPoint.startTCP(context.Background()) + go entryPoint.StartTCP(context.Background()) router := &tcp.Router{} router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) { @@ -123,7 +123,7 @@ func TestShutdownTCPConn(t *testing.T) { require.NoError(t, err) })) - entryPoint.switchRouter(router) + entryPoint.SwitchRouter(router) conn, err := net.Dial("tcp", entryPoint.listener.Addr().String()) require.NoError(t, err) diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go deleted file mode 100644 index 868e282be..000000000 --- a/pkg/server/server_test.go +++ /dev/null @@ -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") - }) - } -} diff --git a/pkg/server/service/internalhandler.go b/pkg/server/service/internalhandler.go new file mode 100644 index 000000000..56b9e66d9 --- /dev/null +++ b/pkg/server/service/internalhandler.go @@ -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) + } +} diff --git a/pkg/server/service/managerfactory.go b/pkg/server/service/managerfactory.go new file mode 100644 index 000000000..d939e8ce1 --- /dev/null +++ b/pkg/server/service/managerfactory.go @@ -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) +} diff --git a/pkg/server/roundtripper.go b/pkg/server/service/roundtripper.go similarity index 90% rename from pkg/server/roundtripper.go rename to pkg/server/service/roundtripper.go index 165476029..f232049a6 100644 --- a/pkg/server/roundtripper.go +++ b/pkg/server/service/roundtripper.go @@ -1,4 +1,4 @@ -package server +package service import ( "crypto/tls" @@ -98,3 +98,13 @@ func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool { 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 +} diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 9c000b171..519213879 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -34,7 +34,7 @@ const ( ) // 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{ routinePool: routinePool, metricsRegistry: metricsRegistry, @@ -42,8 +42,6 @@ func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper htt defaultRoundTripper: defaultRoundTripper, balancers: make(map[string][]healthcheck.BalancerHandler), configs: configs, - api: api, - rest: rest, } } @@ -55,26 +53,10 @@ type Manager struct { defaultRoundTripper http.RoundTripper balancers map[string][]healthcheck.BalancerHandler configs map[string]*runtime.ServiceInfo - api http.Handler - rest http.Handler } // 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) { - 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)) serviceName = internal.GetQualifiedName(ctx, serviceName) diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index 6090266c0..6e1349496 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -80,7 +80,7 @@ func TestGetLoadBalancer(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) { 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.Parallel() - manager := NewManager(test.configs, http.DefaultTransport, nil, nil, nil, nil) + manager := NewManager(test.configs, http.DefaultTransport, nil, nil) ctx := context.Background() if len(test.providerName) > 0 { @@ -346,14 +346,16 @@ func TestManager_Build(t *testing.T) { } func TestMultipleTypeOnBuildHTTP(t *testing.T) { - manager := NewManager(map[string]*runtime.ServiceInfo{ + services := map[string]*runtime.ServiceInfo{ "test@file": { Service: &dynamic.Service{ LoadBalancer: &dynamic.ServersLoadBalancer{}, Weighted: &dynamic.WeightedRoundRobin{}, }, }, - }, http.DefaultTransport, nil, nil, nil, nil) + } + + manager := NewManager(services, http.DefaultTransport, nil, 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") diff --git a/pkg/server/tcprouterfactory.go b/pkg/server/tcprouterfactory.go new file mode 100644 index 000000000..d42d36d9e --- /dev/null +++ b/pkg/server/tcprouterfactory.go @@ -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 +} diff --git a/pkg/server/tcprouterfactory_test.go b/pkg/server/tcprouterfactory_test.go new file mode 100644 index 000000000..115fa84f7 --- /dev/null +++ b/pkg/server/tcprouterfactory_test.go @@ -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") +} diff --git a/pkg/tcp/router.go b/pkg/tcp/router.go index f8a7348b0..f2b3d8e88 100644 --- a/pkg/tcp/router.go +++ b/pkg/tcp/router.go @@ -28,7 +28,7 @@ type Router struct { func (r *Router) ServeTCP(conn WriteCloser) { // 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) return } @@ -184,6 +184,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) { } return "", false, "" } + const recordTypeHandshake = 0x16 if hdr[0] != recordTypeHandshake { // 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) return "", false, getPeeked(br) } + recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3] helloBytes, err := br.Peek(recordHeaderLen + recLen) if err != nil { log.Errorf("Error while Hello: %s", err) return "", true, getPeeked(br) } + sni := "" server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{ GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { @@ -210,6 +213,7 @@ func clientHelloServerName(br *bufio.Reader) (string, bool, string) { }, }) _ = server.Handshake() + return sni, true, getPeeked(br) } diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index e69fdb2df..6e81a2fdb 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -15,6 +15,9 @@ import ( "github.com/sirupsen/logrus" ) +// DefaultTLSOptions the default TLS options. +var DefaultTLSOptions = Options{} + // Manager is the TLS option/store/configuration factory type Manager struct { storesConfig map[string]Store @@ -27,7 +30,12 @@ type Manager struct { // NewManager creates a new Manager func NewManager() *Manager { - return &Manager{} + return &Manager{ + stores: map[string]*CertificateStore{}, + configs: map[string]Options{ + "default": DefaultTLSOptions, + }, + } } // UpdateConfigs updates the TLS* configuration options diff --git a/pkg/types/metrics.go b/pkg/types/metrics.go index 305284023..de0c08283 100644 --- a/pkg/types/metrics.go +++ b/pkg/types/metrics.go @@ -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"` 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"` + ManualRouting bool `description:"Manual routing" json:"manualRouting,omitempty" toml:"manualRouting,omitempty" yaml:"manualRouting,omitempty"` } // SetDefaults sets the default values. diff --git a/pkg/types/tls.go b/pkg/types/tls.go index 2610345c0..21aa0f6e6 100644 --- a/pkg/types/tls.go +++ b/pkg/types/tls.go @@ -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) } } 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 { 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}, RootCAs: caPool, InsecureSkipVerify: clientTLS.InsecureSkipVerify, ClientAuth: clientAuth, - } - return TLSConfig, nil + }, nil } diff --git a/webui/quasar.conf.js b/webui/quasar.conf.js index b25ee4f02..1be691cf4 100644 --- a/webui/quasar.conf.js +++ b/webui/quasar.conf.js @@ -115,7 +115,7 @@ module.exports = function (ctx) { supportIE: false, build: { - publicPath: process.env.APP_PUBLIC_PATH || '/dashboard', + publicPath: process.env.APP_PUBLIC_PATH || '', env: process.env.APP_ENV === 'development' ? { // staging: APP_ENV: JSON.stringify(process.env.APP_ENV), diff --git a/webui/src/components/_commons/PanelServiceDetails.vue b/webui/src/components/_commons/PanelServiceDetails.vue index 9bde9603d..dda6434b9 100644 --- a/webui/src/components/_commons/PanelServiceDetails.vue +++ b/webui/src/components/_commons/PanelServiceDetails.vue @@ -3,7 +3,7 @@
-
+
TYPE
+ + +