diff --git a/Gopkg.lock b/Gopkg.lock index 2ed8a7e1a..137690a59 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1489,13 +1489,6 @@ pruneopts = "NUT" revision = "c4434f09ec131ecf30f986d5dcb1636508bfa49a" -[[projects]] - digest = "1:84b9a5318d8ce3b8a9b1509bf15734f4f9dcd4decf9d9e9c7346a16c7b64d49e" - name = "github.com/thoas/stats" - packages = ["."] - pruneopts = "NUT" - revision = "4975baf6a358ed3ddaa42133996e1959f96c9300" - [[projects]] branch = "master" digest = "1:99ce99ce6d6d0cbc5f822cda92095906e01d5546d60999ac839ab008938e4e17" @@ -2304,7 +2297,6 @@ "github.com/stretchr/testify/mock", "github.com/stretchr/testify/require", "github.com/stvp/go-udp-testing", - "github.com/thoas/stats", "github.com/uber/jaeger-client-go", "github.com/uber/jaeger-client-go/config", "github.com/uber/jaeger-client-go/zipkin", diff --git a/docs/content/contributing/documentation.md b/docs/content/contributing/documentation.md index c64ecf6d6..eb5f176f1 100644 --- a/docs/content/contributing/documentation.md +++ b/docs/content/contributing/documentation.md @@ -10,7 +10,7 @@ Let's see how. ### General -This [documentation](http://docs.traefik.io/) is built with [mkdocs](http://mkdocs.org/). +This [documentation](https://docs.traefik.io/) is built with [mkdocs](https://mkdocs.org/). ### Method 1: `Docker` and `make` diff --git a/integration/access_log_test.go b/integration/access_log_test.go index 606f774bb..397a60eeb 100644 --- a/integration/access_log_test.go +++ b/integration/access_log_test.go @@ -603,7 +603,7 @@ func checkAccessLogExactValues(c *check.C, line string, i int, v accessLogValue) func waitForTraefik(c *check.C, containerName string) { // Wait for Traefik to turn ready. - req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/providers/docker/routers", nil) + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/rawdata", nil) c.Assert(err, checker.IsNil) err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains(containerName)) diff --git a/integration/acme_test.go b/integration/acme_test.go index 8ea4bba10..6b72a95f8 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -333,7 +333,7 @@ func (s *AcmeSuite) TestNoValidLetsEncryptServer(c *check.C) { defer cmd.Process.Kill() // Expected traefik works - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 10*time.Second, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) } diff --git a/integration/docker_compose_test.go b/integration/docker_compose_test.go index bd156b0b9..b0173279f 100644 --- a/integration/docker_compose_test.go +++ b/integration/docker_compose_test.go @@ -62,27 +62,23 @@ func (s *DockerComposeSuite) TestComposeScale(c *check.C) { _, err = try.ResponseUntilStatusCode(req, 1500*time.Millisecond, http.StatusOK) c.Assert(err, checker.IsNil) - resp, err := http.Get("http://127.0.0.1:8080/api/providers/docker/services") + resp, err := http.Get("http://127.0.0.1:8080/api/rawdata") c.Assert(err, checker.IsNil) defer resp.Body.Close() - var services []api.ServiceRepresentation - err = json.NewDecoder(resp.Body).Decode(&services) - c.Assert(err, checker.IsNil) - - // check that we have only one service with n servers - c.Assert(services, checker.HasLen, 1) - c.Assert(services[0].ID, checker.Equals, composeService+"_integrationtest"+composeProject) - c.Assert(services[0].LoadBalancer.Servers, checker.HasLen, serviceCount) - - resp, err = http.Get("http://127.0.0.1:8080/api/providers/docker/routers") - c.Assert(err, checker.IsNil) - defer resp.Body.Close() - - var routers []api.RouterRepresentation - err = json.NewDecoder(resp.Body).Decode(&routers) + var rtconf api.RunTimeRepresentation + err = json.NewDecoder(resp.Body).Decode(&rtconf) c.Assert(err, checker.IsNil) // check that we have only one router - c.Assert(routers, checker.HasLen, 1) + c.Assert(rtconf.Routers, checker.HasLen, 1) + + // check that we have only one service with n servers + services := rtconf.Services + c.Assert(services, checker.HasLen, 1) + for k, v := range services { + c.Assert(k, checker.Equals, "docker."+composeService+"_integrationtest"+composeProject) + c.Assert(v.LoadBalancer.Servers, checker.HasLen, serviceCount) + // We could break here, but we don't just to keep us honest. + } } diff --git a/integration/docker_test.go b/integration/docker_test.go index 78187f53d..b60a5d0c0 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -315,7 +315,7 @@ func (s *DockerSuite) TestRestartDockerContainers(c *check.C) { c.Assert(json.Unmarshal(body, &version), checker.IsNil) c.Assert(version["Version"], checker.Equals, "swarm/1.0.0") - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/services", 60*time.Second, try.BodyContains("powpow")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("powpow")) c.Assert(err, checker.IsNil) s.stopAndRemoveContainerByName(c, "powpow") @@ -323,11 +323,11 @@ func (s *DockerSuite) TestRestartDockerContainers(c *check.C) { time.Sleep(5 * time.Second) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/services", 10*time.Second, try.BodyContains("powpow")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("powpow")) c.Assert(err, checker.NotNil) s.startContainerWithNameAndLabels(c, "powpow", "swarm:1.0.0", labels, "manage", "token://blabla") - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/services", 60*time.Second, try.BodyContains("powpow")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("powpow")) c.Assert(err, checker.IsNil) } diff --git a/integration/fixtures/grpc/config_with_flush.toml b/integration/fixtures/grpc/config_with_flush.toml deleted file mode 100644 index c88ed6914..000000000 --- a/integration/fixtures/grpc/config_with_flush.toml +++ /dev/null @@ -1,34 +0,0 @@ -[global] -checkNewVersion = false -sendAnonymousUsage = false - -[log] -level = "DEBUG" - -[serversTransport] -rootCAs = [ """{{ .CertContent }}""" ] - -[entryPoints] - [entryPoints.web-secure] - address = ":4443" - -[api] - -[providers] - [providers.file] - -[http.routers] - [http.routers.router1] - rule = "Host(`127.0.0.1`)" - service = "service1" - [http.routers.router1.tls] - -[http.services] - [http.services.service1.loadbalancer] - [[http.services.service1.loadbalancer.servers]] - url = "https://127.0.0.1:{{ .GRPCServerPort }}" - weight = 1 - -[tlsStores.default.DefaultCertificate] - certFile = """{{ .CertContent }}""" - keyFile = """{{ .KeyContent }}""" diff --git a/integration/grpc_test.go b/integration/grpc_test.go index d46a88932..e2d4c63e3 100644 --- a/integration/grpc_test.go +++ b/integration/grpc_test.go @@ -167,7 +167,7 @@ func (s *GRPCSuite) TestGRPC(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var response string @@ -205,7 +205,7 @@ func (s *GRPCSuite) TestGRPCh2c(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var response string @@ -247,7 +247,7 @@ func (s *GRPCSuite) TestGRPCh2cTermination(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var response string @@ -289,7 +289,7 @@ func (s *GRPCSuite) TestGRPCInsecure(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var response string @@ -336,7 +336,7 @@ func (s *GRPCSuite) TestGRPCBuffer(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var client helloworld.Greeter_StreamExampleClient client, closer, err := callStreamExampleClientGRPC() @@ -364,7 +364,6 @@ func (s *GRPCSuite) TestGRPCBuffer(c *check.C) { func (s *GRPCSuite) TestGRPCBufferWithFlushInterval(c *check.C) { stopStreamExample := make(chan bool) - lis, err := net.Listen("tcp", ":0") c.Assert(err, check.IsNil) _, port, err := net.SplitHostPort(lis.Addr().String()) @@ -378,7 +377,7 @@ func (s *GRPCSuite) TestGRPCBufferWithFlushInterval(c *check.C) { c.Assert(err, check.IsNil) }() - file := s.adaptFile(c, "fixtures/grpc/config_with_flush.toml", struct { + file := s.adaptFile(c, "fixtures/grpc/config.toml", struct { CertContent string KeyContent string GRPCServerPort string @@ -396,7 +395,7 @@ func (s *GRPCSuite) TestGRPCBufferWithFlushInterval(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var client helloworld.Greeter_StreamExampleClient @@ -454,7 +453,7 @@ func (s *GRPCSuite) TestGRPCWithRetry(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`127.0.0.1`)")) c.Assert(err, check.IsNil) var response string diff --git a/integration/healthcheck_test.go b/integration/healthcheck_test.go index 1729a382f..2d31f149e 100644 --- a/integration/healthcheck_test.go +++ b/integration/healthcheck_test.go @@ -41,7 +41,7 @@ func (s *HealthCheckSuite) TestSimpleConfiguration(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 60*time.Second, try.BodyContains("Host(`test.localhost`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`test.localhost`)")) c.Assert(err, checker.IsNil) frontendHealthReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/health", nil) @@ -117,7 +117,7 @@ func (s *HealthCheckSuite) doTestMultipleEntrypoints(c *check.C, fixture string) defer cmd.Process.Kill() // Wait for traefik - err = try.GetRequest("http://localhost:8080/api/providers/file/routers", 60*time.Second, try.BodyContains("Host(`test.localhost`)")) + err = try.GetRequest("http://localhost:8080/api/rawdata", 60*time.Second, try.BodyContains("Host(`test.localhost`)")) c.Assert(err, checker.IsNil) // Check entrypoint http1 @@ -194,7 +194,7 @@ func (s *HealthCheckSuite) TestPortOverload(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 10*time.Second, try.BodyContains("Host(`test.localhost`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("Host(`test.localhost`)")) c.Assert(err, checker.IsNil) frontendHealthReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/health", nil) diff --git a/integration/https_test.go b/integration/https_test.go index 6ea6041c6..8e0118f6c 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -32,7 +32,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.org`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -66,7 +66,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) backend1 := startTestServer("9010", http.StatusNoContent) @@ -122,7 +122,7 @@ func (s *HTTPSSuite) TestWithSNIStrictNotMatchedRequest(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -146,7 +146,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificate(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -180,7 +180,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -214,7 +214,7 @@ func (s *HTTPSSuite) TestWithOverlappingStaticCertificate(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -249,7 +249,7 @@ func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.com`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -282,7 +282,7 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthentication(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("Host(`snitest.org`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -338,7 +338,7 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAs(c *check defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -399,7 +399,7 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipleCAsMultipleF defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ @@ -464,7 +464,7 @@ func (s *HTTPSSuite) TestWithRootCAsContentForHTTPSOnBackend(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 1*time.Second, try.BodyContains(backend.URL)) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(backend.URL)) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8081/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -486,7 +486,7 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 1*time.Second, try.BodyContains(backend.URL)) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(backend.URL)) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8081/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -544,7 +544,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { } // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) backend1 := startTestServer("9010", http.StatusNoContent) @@ -613,7 +613,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { } // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) backend1 := startTestServer("9010", http.StatusNoContent) @@ -676,7 +676,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c } // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) backend2 := startTestServer("9020", http.StatusResetContent) @@ -740,7 +740,7 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 5*time.Second, try.BodyContains("Host(`example.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.BodyContains("Host(`example.com`)")) c.Assert(err, checker.IsNil) client := &http.Client{ @@ -841,7 +841,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("HostRegexp(`{subdomain:[a-z1-9-]+}.www.snitest.com`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("HostRegexp(`{subdomain:[a-z1-9-]+}.www.snitest.com`)")) c.Assert(err, checker.IsNil) tlsConfig := &tls.Config{ diff --git a/integration/k8s_test.go b/integration/k8s_test.go index f43e5503f..07ca7e7be 100644 --- a/integration/k8s_test.go +++ b/integration/k8s_test.go @@ -74,9 +74,9 @@ func (s *K8sSuite) TestCRDSimple(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("PathPrefix(`/tobestripped`)")) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/kubernetescrd/routers", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("default/stripprefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("default/stripprefix")) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/kubernetescrd/middlewares", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("stripprefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("stripprefix")) c.Assert(err, checker.IsNil) } diff --git a/integration/resources/compose/access_log.yml b/integration/resources/compose/access_log.yml index c000a2f01..34eb5caa9 100644 --- a/integration/resources/compose/access_log.yml +++ b/integration/resources/compose/access_log.yml @@ -11,6 +11,7 @@ server1: - traefik.enable=true - traefik.http.routers.rt-server1.entryPoints=web - traefik.http.routers.rt-server1.rule=Host("frontend1.docker.local") + - traefik.http.routers.rt-server1.service=service1 - traefik.http.services.service1.loadbalancer.server.port=80 server2: image: containous/whoami diff --git a/integration/rest_test.go b/integration/rest_test.go index 5f226bd87..8e31b559c 100644 --- a/integration/rest_test.go +++ b/integration/rest_test.go @@ -65,7 +65,7 @@ func (s *RestSuite) TestSimpleConfiguration(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(response.StatusCode, checker.Equals, http.StatusOK) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/rest/routers", 1000*time.Millisecond, try.BodyContains("PathPrefix(`/`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("PathPrefix(`/`)")) 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/retry_test.go b/integration/retry_test.go index f02bbd074..a2d404d16 100644 --- a/integration/retry_test.go +++ b/integration/retry_test.go @@ -31,7 +31,7 @@ func (s *RetrySuite) TestRetry(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 60*time.Second, try.BodyContains("PathPrefix(`/`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("PathPrefix(`/`)")) c.Assert(err, checker.IsNil) // This simulates a DialTimeout when connecting to the backend server. @@ -53,7 +53,7 @@ func (s *RetrySuite) TestRetryWebsocket(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 60*time.Second, try.BodyContains("PathPrefix(`/`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("PathPrefix(`/`)")) c.Assert(err, checker.IsNil) // This simulates a DialTimeout when connecting to the backend server. diff --git a/integration/simple_test.go b/integration/simple_test.go index 2ae6e7d30..5e9048554 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -60,7 +60,7 @@ func (s *SimpleSuite) TestWithWebConfig(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) } @@ -173,10 +173,10 @@ func (s *SimpleSuite) TestApiOnSameEntryPoint(c *check.C) { err = try.GetRequest("http://127.0.0.1:8000/test", 1*time.Second, try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8000/api/providers/docker", 1*time.Second, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8000/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8000/api/providers/docker/routers", 1*time.Second, try.BodyContains("PathPrefix")) + err = try.GetRequest("http://127.0.0.1:8000/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -205,7 +205,7 @@ func (s *SimpleSuite) TestStatsWithMultipleEntryPoint(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api", 1*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("PathPrefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -230,7 +230,7 @@ func (s *SimpleSuite) TestNoAuthOnPing(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8001/api/providers", 2*time.Second, try.StatusCodeIs(http.StatusUnauthorized)) + err = try.GetRequest("http://127.0.0.1:8001/api/rawdata", 2*time.Second, try.StatusCodeIs(http.StatusUnauthorized)) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8001/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -248,7 +248,7 @@ func (s *SimpleSuite) TestDefaultEntrypointHTTP(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 1*time.Second, try.BodyContains("PathPrefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -266,7 +266,7 @@ func (s *SimpleSuite) TestWithUnexistingEntrypoint(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 1*time.Second, try.BodyContains("PathPrefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -284,7 +284,7 @@ func (s *SimpleSuite) TestMetricsPrometheusDefaultEntrypoint(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 1*time.Second, try.BodyContains("PathPrefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK)) @@ -312,7 +312,7 @@ func (s *SimpleSuite) TestMultipleProviderSameBackendName(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 1*time.Second, try.BodyContains("PathPrefix")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.BodyContains(ipWhoami01)) @@ -334,10 +334,10 @@ func (s *SimpleSuite) TestIPStrategyWhitelist(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 2*time.Second, try.BodyContains("override")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 2*time.Second, try.BodyContains("override")) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 2*time.Second, try.BodyContains("override.remoteaddr.whitelist.docker.local")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 2*time.Second, try.BodyContains("override.remoteaddr.whitelist.docker.local")) c.Assert(err, checker.IsNil) testCases := []struct { @@ -415,7 +415,7 @@ func (s *SimpleSuite) TestXForwardedHeaders(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 2*time.Second, + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 2*time.Second, try.BodyContains("override.remoteaddr.whitelist.docker.local")) c.Assert(err, checker.IsNil) @@ -450,7 +450,7 @@ func (s *SimpleSuite) TestMultiprovider(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 1000*time.Millisecond, try.BodyContains("service")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("service")) c.Assert(err, checker.IsNil) config := config.HTTPConfiguration{ @@ -474,7 +474,7 @@ func (s *SimpleSuite) TestMultiprovider(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(response.StatusCode, checker.Equals, http.StatusOK) - err = try.GetRequest("http://127.0.0.1:8080/api/providers/rest/routers", 1000*time.Millisecond, try.BodyContains("PathPrefix(`/`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("PathPrefix(`/`)")) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("CustomValue")) diff --git a/integration/tcp_test.go b/integration/tcp_test.go index 4b634b5c9..b9d197cfd 100644 --- a/integration/tcp_test.go +++ b/integration/tcp_test.go @@ -31,7 +31,7 @@ func (s *TCPSuite) TestMixed(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.BodyContains("Path(`/test`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.BodyContains("Path(`/test`)")) c.Assert(err, checker.IsNil) //Traefik passes through, termination handled by whoami-a diff --git a/integration/timeout_test.go b/integration/timeout_test.go index d45d59abc..43db8761c 100644 --- a/integration/timeout_test.go +++ b/integration/timeout_test.go @@ -31,7 +31,7 @@ func (s *TimeoutSuite) TestForwardingTimeouts(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 60*time.Second, try.BodyContains("Path(`/dialTimeout`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.BodyContains("Path(`/dialTimeout`)")) c.Assert(err, checker.IsNil) // This simulates a DialTimeout when connecting to the backend server. diff --git a/integration/tls_client_headers_test.go b/integration/tls_client_headers_test.go index 83ba22add..f46ed660d 100644 --- a/integration/tls_client_headers_test.go +++ b/integration/tls_client_headers_test.go @@ -50,7 +50,7 @@ func (s *TLSClientHeadersSuite) TestTLSClientHeaders(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/providers/docker/routers", 2*time.Second, try.BodyContains("PathPrefix(`/`)")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 2*time.Second, try.BodyContains("PathPrefix(`/`)")) c.Assert(err, checker.IsNil) request, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:8443", nil) diff --git a/integration/tracing_test.go b/integration/tracing_test.go index 5bd8b8a43..d7f5dee96 100644 --- a/integration/tracing_test.go +++ b/integration/tracing_test.go @@ -59,6 +59,10 @@ func (s *TracingSuite) TestZipkinRateLimit(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", time.Second, try.BodyContains("basic-auth")) + c.Assert(err, checker.IsNil) + err = try.GetRequest("http://127.0.0.1:8000/ratelimit", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8000/ratelimit", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) @@ -106,6 +110,10 @@ func (s *TracingSuite) TestZipkinRetry(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", time.Second, try.BodyContains("basic-auth")) + c.Assert(err, checker.IsNil) + err = try.GetRequest("http://127.0.0.1:8000/retry", 500*time.Millisecond, try.StatusCodeIs(http.StatusBadGateway)) c.Assert(err, checker.IsNil) @@ -129,6 +137,10 @@ func (s *TracingSuite) TestZipkinAuth(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", time.Second, try.BodyContains("basic-auth")) + c.Assert(err, checker.IsNil) + err = try.GetRequest("http://127.0.0.1:8000/auth", 500*time.Millisecond, try.StatusCodeIs(http.StatusUnauthorized)) c.Assert(err, checker.IsNil) diff --git a/integration/websocket_test.go b/integration/websocket_test.go index a117ce01d..6dfca08e3 100644 --- a/integration/websocket_test.go +++ b/integration/websocket_test.go @@ -57,7 +57,7 @@ func (s *WebsocketSuite) TestBase(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil) @@ -107,7 +107,7 @@ func (s *WebsocketSuite) TestWrongOrigin(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:800") @@ -157,7 +157,7 @@ func (s *WebsocketSuite) TestOrigin(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:8000") @@ -218,7 +218,7 @@ func (s *WebsocketSuite) TestWrongOriginIgnoredByServer(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:80") @@ -276,7 +276,7 @@ func (s *WebsocketSuite) TestSSLTermination(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) // Add client self-signed cert @@ -339,7 +339,7 @@ func (s *WebsocketSuite) TestBasicAuth(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:8000") @@ -383,7 +383,7 @@ func (s *WebsocketSuite) TestSpecificResponseFromBackend(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) _, resp, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil) @@ -429,7 +429,7 @@ func (s *WebsocketSuite) TestURLWithURLEncodedChar(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws/http%3A%2F%2Ftest", nil) @@ -484,7 +484,7 @@ func (s *WebsocketSuite) TestSSLhttp2(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) // Add client self-signed cert @@ -543,7 +543,7 @@ func (s *WebsocketSuite) TestHeaderAreForwared(c *check.C) { defer cmd.Process.Kill() // wait for traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 10*time.Second, try.BodyContains("127.0.0.1")) + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 10*time.Second, try.BodyContains("127.0.0.1")) c.Assert(err, checker.IsNil) headers := http.Header{} diff --git a/pkg/api/handler.go b/pkg/api/handler.go index e920911f1..89ef75cb6 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -6,79 +6,70 @@ import ( "github.com/containous/mux" "github.com/containous/traefik/pkg/config" + "github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/log" - "github.com/containous/traefik/pkg/safe" "github.com/containous/traefik/pkg/types" "github.com/containous/traefik/pkg/version" assetfs "github.com/elazarl/go-bindata-assetfs" - thoasstats "github.com/thoas/stats" "github.com/unrolled/render" ) -// ResourceIdentifier a resource identifier -type ResourceIdentifier struct { - ID string `json:"id"` - Path string `json:"path"` -} - -// ProviderRepresentation a provider with resource identifiers -type ProviderRepresentation struct { - Routers []ResourceIdentifier `json:"routers,omitempty"` - Middlewares []ResourceIdentifier `json:"middlewares,omitempty"` - Services []ResourceIdentifier `json:"services,omitempty"` -} - -// RouterRepresentation extended version of a router configuration with an ID -type RouterRepresentation struct { - *config.Router - ID string `json:"id"` -} - -// MiddlewareRepresentation extended version of a middleware configuration with an ID -type MiddlewareRepresentation struct { - *config.Middleware - ID string `json:"id"` -} - -// ServiceRepresentation extended version of a service configuration with an ID -type ServiceRepresentation struct { - *config.Service - ID string `json:"id"` -} - -// Handler expose api routes -type Handler struct { - EntryPoint string - Dashboard bool - Debug bool - CurrentConfigurations *safe.Safe - Statistics *types.Statistics - Stats *thoasstats.Stats - // StatsRecorder *middlewares.StatsRecorder // FIXME stats - DashboardAssets *assetfs.AssetFS -} - var templateRenderer jsonRenderer = render.New(render.Options{Directory: "nowhere"}) type jsonRenderer interface { JSON(w io.Writer, status int, v interface{}) error } +type serviceInfoRepresentation struct { + *config.ServiceInfo + ServerStatus map[string]string `json:"serverStatus,omitempty"` +} + +// RunTimeRepresentation is the configuration information exposed by the API handler. +type RunTimeRepresentation struct { + Routers map[string]*config.RouterInfo `json:"routers,omitempty"` + Middlewares map[string]*config.MiddlewareInfo `json:"middlewares,omitempty"` + Services map[string]*serviceInfoRepresentation `json:"services,omitempty"` + TCPRouters map[string]*config.TCPRouterInfo `json:"tcpRouters,omitempty"` + TCPServices map[string]*config.TCPServiceInfo `json:"tcpServices,omitempty"` +} + +// Handler serves the configuration and status of Traefik on API endpoints. +type Handler struct { + dashboard bool + debug bool + // runtimeConfiguration is the data set used to create all the data representations exposed by the API. + runtimeConfiguration *config.RuntimeConfiguration + statistics *types.Statistics + // stats *thoasstats.Stats // FIXME stats + // StatsRecorder *middlewares.StatsRecorder // FIXME stats + dashboardAssets *assetfs.AssetFS +} + +// New returns a Handler defined by staticConfig, and if provided, by runtimeConfig. +// It finishes populating the information provided in the runtimeConfig. +func New(staticConfig static.Configuration, runtimeConfig *config.RuntimeConfiguration) *Handler { + rConfig := runtimeConfig + if rConfig == nil { + rConfig = &config.RuntimeConfiguration{} + } + + return &Handler{ + dashboard: staticConfig.API.Dashboard, + statistics: staticConfig.API.Statistics, + dashboardAssets: staticConfig.API.DashboardAssets, + runtimeConfiguration: rConfig, + debug: staticConfig.Global.Debug, + } +} + // Append add api routes on a router func (h Handler) Append(router *mux.Router) { - if h.Debug { + if h.debug { DebugHandler{}.Append(router) } - router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRawData) - router.Methods(http.MethodGet).Path("/api/providers").HandlerFunc(h.getProvidersHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}").HandlerFunc(h.getProviderHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}/routers").HandlerFunc(h.getRoutersHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}/routers/{router}").HandlerFunc(h.getRouterHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}/middlewares").HandlerFunc(h.getMiddlewaresHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}/middlewares/{middleware}").HandlerFunc(h.getMiddlewareHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}/services").HandlerFunc(h.getServicesHandler) - router.Methods(http.MethodGet).Path("/api/providers/{provider}/services/{service}").HandlerFunc(h.getServiceHandler) + router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration) // FIXME stats // health route @@ -86,268 +77,29 @@ func (h Handler) Append(router *mux.Router) { version.Handler{}.Append(router) - if h.Dashboard { - DashboardHandler{Assets: h.DashboardAssets}.Append(router) + if h.dashboard { + DashboardHandler{Assets: h.dashboardAssets}.Append(router) } } -func (h Handler) getRawData(rw http.ResponseWriter, request *http.Request) { - if h.CurrentConfigurations != nil { - currentConfigurations, ok := h.CurrentConfigurations.Get().(config.Configurations) - if !ok { - rw.WriteHeader(http.StatusOK) - return - } - err := templateRenderer.JSON(rw, http.StatusOK, currentConfigurations) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) +func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) { + siRepr := make(map[string]*serviceInfoRepresentation, len(h.runtimeConfiguration.Services)) + for k, v := range h.runtimeConfiguration.Services { + siRepr[k] = &serviceInfoRepresentation{ + ServiceInfo: v, + ServerStatus: v.GetAllStatus(), } } -} -func (h Handler) getProvidersHandler(rw http.ResponseWriter, request *http.Request) { - // FIXME handle currentConfiguration - if h.CurrentConfigurations != nil { - currentConfigurations, ok := h.CurrentConfigurations.Get().(config.Configurations) - if !ok { - rw.WriteHeader(http.StatusOK) - return - } - - var providers []ResourceIdentifier - for name := range currentConfigurations { - providers = append(providers, ResourceIdentifier{ - ID: name, - Path: "/api/providers/" + name, - }) - } - - err := templateRenderer.JSON(rw, http.StatusOK, providers) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } - } -} - -func (h Handler) getProviderHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return + rtRepr := RunTimeRepresentation{ + Routers: h.runtimeConfiguration.Routers, + Middlewares: h.runtimeConfiguration.Middlewares, + Services: siRepr, + TCPRouters: h.runtimeConfiguration.TCPRouters, + TCPServices: h.runtimeConfiguration.TCPServices, } - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - var routers []ResourceIdentifier - for name := range provider.HTTP.Routers { - routers = append(routers, ResourceIdentifier{ - ID: name, - Path: "/api/providers/" + providerID + "/routers", - }) - } - - var services []ResourceIdentifier - for name := range provider.HTTP.Services { - services = append(services, ResourceIdentifier{ - ID: name, - Path: "/api/providers/" + providerID + "/services", - }) - } - - var middlewares []ResourceIdentifier - for name := range provider.HTTP.Middlewares { - middlewares = append(middlewares, ResourceIdentifier{ - ID: name, - Path: "/api/providers/" + providerID + "/middlewares", - }) - } - - providers := ProviderRepresentation{Routers: routers, Middlewares: middlewares, Services: services} - - err := templateRenderer.JSON(rw, http.StatusOK, providers) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } -} - -func (h Handler) getRoutersHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return - } - - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - var routers []RouterRepresentation - for name, router := range provider.HTTP.Routers { - routers = append(routers, RouterRepresentation{Router: router, ID: name}) - } - - err := templateRenderer.JSON(rw, http.StatusOK, routers) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } -} - -func (h Handler) getRouterHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - routerID := mux.Vars(request)["router"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return - } - - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - router, ok := provider.HTTP.Routers[routerID] - if !ok { - http.NotFound(rw, request) - return - } - - err := templateRenderer.JSON(rw, http.StatusOK, router) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } -} - -func (h Handler) getMiddlewaresHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return - } - - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - var middlewares []MiddlewareRepresentation - for name, middleware := range provider.HTTP.Middlewares { - middlewares = append(middlewares, MiddlewareRepresentation{Middleware: middleware, ID: name}) - } - - err := templateRenderer.JSON(rw, http.StatusOK, middlewares) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } -} - -func (h Handler) getMiddlewareHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - middlewareID := mux.Vars(request)["middleware"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return - } - - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - middleware, ok := provider.HTTP.Middlewares[middlewareID] - if !ok { - http.NotFound(rw, request) - return - } - - err := templateRenderer.JSON(rw, http.StatusOK, middleware) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } -} - -func (h Handler) getServicesHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return - } - - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - var services []ServiceRepresentation - for name, service := range provider.HTTP.Services { - services = append(services, ServiceRepresentation{Service: service, ID: name}) - } - - err := templateRenderer.JSON(rw, http.StatusOK, services) - if err != nil { - log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) - } -} - -func (h Handler) getServiceHandler(rw http.ResponseWriter, request *http.Request) { - providerID := mux.Vars(request)["provider"] - serviceID := mux.Vars(request)["service"] - - currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations) - - provider, ok := currentConfigurations[providerID] - if !ok { - http.NotFound(rw, request) - return - } - - if provider.HTTP == nil { - http.NotFound(rw, request) - return - } - - service, ok := provider.HTTP.Services[serviceID] - if !ok { - http.NotFound(rw, request) - return - } - - err := templateRenderer.JSON(rw, http.StatusOK, service) + err := templateRenderer.JSON(rw, http.StatusOK, rtRepr) if err != nil { log.FromContext(request.Context()).Error(err) http.Error(rw, err.Error(), http.StatusInternalServerError) diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go index c195d9ef0..034ac9365 100644 --- a/pkg/api/handler_test.go +++ b/pkg/api/handler_test.go @@ -1,6 +1,8 @@ package api import ( + "encoding/json" + "flag" "io/ioutil" "net/http" "net/http/httptest" @@ -8,188 +10,122 @@ import ( "github.com/containous/mux" "github.com/containous/traefik/pkg/config" - "github.com/containous/traefik/pkg/safe" + "github.com/containous/traefik/pkg/config/static" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var updateExpected = flag.Bool("update_expected", false, "Update expected files in testdata") + func TestHandler_Configuration(t *testing.T) { type expected struct { statusCode int - body string + json string } testCases := []struct { - desc string - path string - configuration config.Configurations - expected expected + desc string + path string + conf config.RuntimeConfiguration + expected expected }{ { - desc: "Get all the providers", - path: "/api/providers", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Routers: map[string]*config.Router{ - "bar": {EntryPoints: []string{"foo", "bar"}}, - }, - }, - }, - }, - expected: expected{statusCode: http.StatusOK, body: `[{"id":"foo","path":"/api/providers/foo"}]`}, - }, - { - desc: "Get a provider", - path: "/api/providers/foo", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Routers: map[string]*config.Router{ - "bar": {EntryPoints: []string{"foo", "bar"}}, - }, - Middlewares: map[string]*config.Middleware{ - "bar": { - AddPrefix: &config.AddPrefix{Prefix: "bar"}, - }, - }, - Services: map[string]*config.Service{ - "foo": { - LoadBalancer: &config.LoadBalancerService{ - Method: "wrr", + desc: "Get rawdata", + path: "/api/rawdata", + conf: config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, }, + Method: "wrr", }, }, }, }, - }, - expected: expected{statusCode: http.StatusOK, body: `{"routers":[{"id":"bar","path":"/api/providers/foo/routers"}],"middlewares":[{"id":"bar","path":"/api/providers/foo/middlewares"}],"services":[{"id":"foo","path":"/api/providers/foo/services"}]}`}, - }, - { - desc: "Provider not found", - path: "/api/providers/foo", - configuration: config.Configurations{}, - expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"}, - }, - { - desc: "Get all routers", - path: "/api/providers/foo/routers", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Routers: map[string]*config.Router{ - "bar": {EntryPoints: []string{"foo", "bar"}}, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + Middleware: &config.Middleware{ + BasicAuth: &config.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + }, + "myprovider.addPrefixTest": { + Middleware: &config.Middleware{ + AddPrefix: &config.AddPrefix{ + Prefix: "/titi", + }, + }, + }, + "anotherprovider.addPrefixTest": { + Middleware: &config.Middleware{ + AddPrefix: &config.AddPrefix{ + Prefix: "/toto", + }, }, }, }, - }, - expected: expected{statusCode: http.StatusOK, body: `[{"entryPoints":["foo","bar"],"id":"bar"}]`}, - }, - { - desc: "Get a router", - path: "/api/providers/foo/routers/bar", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Routers: map[string]*config.Router{ - "bar": {EntryPoints: []string{"foo", "bar"}}, + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "anotherprovider.addPrefixTest"}, + }, + }, + "myprovider.test": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, }, }, }, - }, - expected: expected{statusCode: http.StatusOK, body: `{"entryPoints":["foo","bar"]}`}, - }, - { - desc: "Router not found", - path: "/api/providers/foo/routers/bar", - configuration: config.Configurations{ - "foo": {}, - }, - expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"}, - }, - { - desc: "Get all services", - path: "/api/providers/foo/services", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Services: map[string]*config.Service{ - "foo": { - LoadBalancer: &config.LoadBalancerService{ - Method: "wrr", + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.tcpfoo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1", + Weight: 1, + }, }, + Method: "wrr", }, }, }, }, - }, - expected: expected{statusCode: http.StatusOK, body: `[{"loadbalancer":{"method":"wrr","passHostHeader":false},"id":"foo"}]`}, - }, - { - desc: "Get a service", - path: "/api/providers/foo/services/foo", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Services: map[string]*config.Service{ - "foo": { - LoadBalancer: &config.LoadBalancerService{ - Method: "wrr", - }, - }, + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.tcpbar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.tcpfoo-service", + Rule: "HostSNI(`foo.bar`)", + }, + }, + "myprovider.tcptest": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.tcpfoo-service", + Rule: "HostSNI(`foo.bar.other`)", }, }, }, }, - expected: expected{statusCode: http.StatusOK, body: `{"loadbalancer":{"method":"wrr","passHostHeader":false}}`}, - }, - { - desc: "Service not found", - path: "/api/providers/foo/services/bar", - configuration: config.Configurations{ - "foo": {}, + + expected: expected{ + statusCode: http.StatusOK, + json: "testdata/getrawdata.json", }, - expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"}, - }, - { - desc: "Get all middlewares", - path: "/api/providers/foo/middlewares", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Middlewares: map[string]*config.Middleware{ - "bar": { - AddPrefix: &config.AddPrefix{Prefix: "bar"}, - }, - }, - }, - }, - }, - expected: expected{statusCode: http.StatusOK, body: `[{"addPrefix":{"prefix":"bar"},"id":"bar"}]`}, - }, - { - desc: "Get a middleware", - path: "/api/providers/foo/middlewares/bar", - configuration: config.Configurations{ - "foo": { - HTTP: &config.HTTPConfiguration{ - Middlewares: map[string]*config.Middleware{ - "bar": { - AddPrefix: &config.AddPrefix{Prefix: "bar"}, - }, - }, - }, - }, - }, - expected: expected{statusCode: http.StatusOK, body: `{"addPrefix":{"prefix":"bar"}}`}, - }, - { - desc: "Middleware not found", - path: "/api/providers/foo/middlewares/bar", - configuration: config.Configurations{ - "foo": {}, - }, - expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"}, }, } @@ -198,15 +134,11 @@ func TestHandler_Configuration(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - currentConfiguration := &safe.Safe{} - currentConfiguration.Set(test.configuration) - - handler := Handler{ - CurrentConfigurations: currentConfiguration, - } - + rtConf := &test.conf + handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) router := mux.NewRouter() handler.Append(router) + rtConf.PopulateUsedBy() server := httptest.NewServer(router) @@ -215,12 +147,30 @@ func TestHandler_Configuration(t *testing.T) { assert.Equal(t, test.expected.statusCode, resp.StatusCode) - content, err := ioutil.ReadAll(resp.Body) + contents, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) + err = resp.Body.Close() require.NoError(t, err) - assert.Equal(t, test.expected.body, string(content)) + if test.expected.json == "" { + return + } + if *updateExpected { + var rtRepr RunTimeRepresentation + err := json.Unmarshal(contents, &rtRepr) + require.NoError(t, err) + + newJSON, err := json.MarshalIndent(rtRepr, "", "\t") + require.NoError(t, err) + + err = ioutil.WriteFile(test.expected.json, newJSON, 0644) + require.NoError(t, err) + } + + data, err := ioutil.ReadFile(test.expected.json) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(contents)) }) } } diff --git a/pkg/api/testdata/getrawdata.json b/pkg/api/testdata/getrawdata.json new file mode 100644 index 000000000..1a22d9496 --- /dev/null +++ b/pkg/api/testdata/getrawdata.json @@ -0,0 +1,106 @@ +{ + "routers": { + "myprovider.bar": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "auth", + "anotherprovider.addPrefixTest" + ], + "service": "myprovider.foo-service", + "rule": "Host(`foo.bar`)" + }, + "myprovider.test": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "addPrefixTest", + "auth" + ], + "service": "myprovider.foo-service", + "rule": "Host(`foo.bar.other`)" + } + }, + "middlewares": { + "anotherprovider.addPrefixTest": { + "addPrefix": { + "prefix": "/toto" + }, + "usedBy": [ + "myprovider.bar" + ] + }, + "myprovider.addPrefixTest": { + "addPrefix": { + "prefix": "/titi" + }, + "usedBy": [ + "myprovider.test" + ] + }, + "myprovider.auth": { + "basicAuth": { + "users": [ + "admin:admin" + ] + }, + "usedBy": [ + "myprovider.bar", + "myprovider.test" + ] + } + }, + "services": { + "myprovider.foo-service": { + "loadbalancer": { + "servers": [ + { + "url": "http://127.0.0.1", + "weight": 1 + } + ], + "method": "wrr", + "passHostHeader": false + }, + "usedBy": [ + "myprovider.bar", + "myprovider.test" + ] + } + }, + "tcpRouters": { + "myprovider.tcpbar": { + "entryPoints": [ + "web" + ], + "service": "myprovider.tcpfoo-service", + "rule": "HostSNI(`foo.bar`)" + }, + "myprovider.tcptest": { + "entryPoints": [ + "web" + ], + "service": "myprovider.tcpfoo-service", + "rule": "HostSNI(`foo.bar.other`)" + } + }, + "tcpServices": { + "myprovider.tcpfoo-service": { + "loadbalancer": { + "servers": [ + { + "address": "127.0.0.1", + "weight": 1 + } + ], + "method": "wrr" + }, + "usedBy": [ + "myprovider.tcpbar", + "myprovider.tcptest" + ] + } + } +} \ No newline at end of file diff --git a/pkg/config/runtime.go b/pkg/config/runtime.go new file mode 100644 index 000000000..b1d2409b6 --- /dev/null +++ b/pkg/config/runtime.go @@ -0,0 +1,210 @@ +package config + +import ( + "sort" + "strings" + "sync" + + "github.com/containous/traefik/pkg/log" +) + +// RuntimeConfiguration holds the information about the currently running traefik instance. +type RuntimeConfiguration struct { + Routers map[string]*RouterInfo `json:"routers,omitempty"` + Middlewares map[string]*MiddlewareInfo `json:"middlewares,omitempty"` + Services map[string]*ServiceInfo `json:"services,omitempty"` + TCPRouters map[string]*TCPRouterInfo `json:"tcpRouters,omitempty"` + TCPServices map[string]*TCPServiceInfo `json:"tcpServices,omitempty"` +} + +// NewRuntimeConfig returns a RuntimeConfiguration initialized with the given conf. It never returns nil. +func NewRuntimeConfig(conf Configuration) *RuntimeConfiguration { + if conf.HTTP == nil && conf.TCP == nil { + return &RuntimeConfiguration{} + } + + runtimeConfig := &RuntimeConfiguration{} + + if conf.HTTP != nil { + routers := conf.HTTP.Routers + if len(routers) > 0 { + runtimeConfig.Routers = make(map[string]*RouterInfo, len(routers)) + for k, v := range routers { + runtimeConfig.Routers[k] = &RouterInfo{Router: v} + } + } + + services := conf.HTTP.Services + if len(services) > 0 { + runtimeConfig.Services = make(map[string]*ServiceInfo, len(services)) + for k, v := range services { + runtimeConfig.Services[k] = &ServiceInfo{Service: v} + } + } + + middlewares := conf.HTTP.Middlewares + if len(middlewares) > 0 { + runtimeConfig.Middlewares = make(map[string]*MiddlewareInfo, len(middlewares)) + for k, v := range middlewares { + runtimeConfig.Middlewares[k] = &MiddlewareInfo{Middleware: v} + } + } + } + + if conf.TCP != nil { + if len(conf.TCP.Routers) > 0 { + runtimeConfig.TCPRouters = make(map[string]*TCPRouterInfo, len(conf.TCP.Routers)) + for k, v := range conf.TCP.Routers { + runtimeConfig.TCPRouters[k] = &TCPRouterInfo{TCPRouter: v} + } + } + + if len(conf.TCP.Services) > 0 { + runtimeConfig.TCPServices = make(map[string]*TCPServiceInfo, len(conf.TCP.Services)) + for k, v := range conf.TCP.Services { + runtimeConfig.TCPServices[k] = &TCPServiceInfo{TCPService: v} + } + } + } + + return runtimeConfig +} + +// PopulateUsedBy populates all the UsedBy lists of the underlying fields of r, +// based on the relations between the included services, routers, and middlewares. +func (r *RuntimeConfiguration) PopulateUsedBy() { + if r == nil { + return + } + + logger := log.WithoutContext() + + for routerName, routerInfo := range r.Routers { + providerName := getProviderName(routerName) + if providerName == "" { + logger.WithField(log.RouterName, routerName).Error("router name is not fully qualified") + continue + } + + for _, midName := range routerInfo.Router.Middlewares { + fullMidName := getQualifiedName(providerName, midName) + if _, ok := r.Middlewares[fullMidName]; !ok { + continue + } + r.Middlewares[fullMidName].UsedBy = append(r.Middlewares[fullMidName].UsedBy, routerName) + } + + serviceName := getQualifiedName(providerName, routerInfo.Router.Service) + if _, ok := r.Services[serviceName]; !ok { + continue + } + r.Services[serviceName].UsedBy = append(r.Services[serviceName].UsedBy, routerName) + } + + for k := range r.Services { + sort.Strings(r.Services[k].UsedBy) + } + + for k := range r.Middlewares { + sort.Strings(r.Middlewares[k].UsedBy) + } + + for routerName, routerInfo := range r.TCPRouters { + providerName := getProviderName(routerName) + if providerName == "" { + logger.WithField(log.RouterName, routerName).Error("tcp router name is not fully qualified") + continue + } + + serviceName := getQualifiedName(providerName, routerInfo.TCPRouter.Service) + if _, ok := r.TCPServices[serviceName]; !ok { + continue + } + r.TCPServices[serviceName].UsedBy = append(r.TCPServices[serviceName].UsedBy, routerName) + } + + for k := range r.TCPServices { + sort.Strings(r.TCPServices[k].UsedBy) + } +} + +// RouterInfo holds information about a currently running HTTP router +type RouterInfo struct { + *Router // dynamic configuration + Err string `json:"error,omitempty"` // initialization error +} + +// TCPRouterInfo holds information about a currently running TCP router +type TCPRouterInfo struct { + *TCPRouter // dynamic configuration + Err string `json:"error,omitempty"` // initialization error +} + +// MiddlewareInfo holds information about a currently running middleware +type MiddlewareInfo struct { + *Middleware // dynamic configuration + Err error `json:"error,omitempty"` // initialization error + UsedBy []string `json:"usedBy,omitempty"` // list of routers and services using that middleware +} + +// ServiceInfo holds information about a currently running service +type ServiceInfo struct { + *Service // dynamic configuration + Err error `json:"error,omitempty"` // initialization error + UsedBy []string `json:"usedBy,omitempty"` // list of routers using that service + + statusMu sync.RWMutex + status map[string]string // keyed by server URL +} + +// UpdateStatus sets the status of the server in the ServiceInfo. +// It is the responsibility of the caller to check that s is not nil. +func (s *ServiceInfo) UpdateStatus(server string, status string) { + s.statusMu.Lock() + defer s.statusMu.Unlock() + + if s.status == nil { + s.status = make(map[string]string) + } + s.status[server] = status +} + +// GetAllStatus returns all the statuses of all the servers in ServiceInfo. +// It is the responsibility of the caller to check that s is not nil +func (s *ServiceInfo) GetAllStatus() map[string]string { + s.statusMu.RLock() + defer s.statusMu.RUnlock() + + if len(s.status) == 0 { + return nil + } + + allStatus := make(map[string]string, len(s.status)) + for k, v := range s.status { + allStatus[k] = v + } + return allStatus +} + +// TCPServiceInfo holds information about a currently running TCP service +type TCPServiceInfo struct { + *TCPService // dynamic configuration + Err error `json:"error,omitempty"` // initialization error + UsedBy []string `json:"usedBy,omitempty"` // list of routers using that service +} + +func getProviderName(elementName string) string { + parts := strings.Split(elementName, ".") + if len(parts) > 1 { + return parts[0] + } + return "" +} + +func getQualifiedName(provider, elementName string) string { + parts := strings.Split(elementName, ".") + if len(parts) == 1 { + return provider + "." + elementName + } + return elementName +} diff --git a/pkg/config/runtime_test.go b/pkg/config/runtime_test.go new file mode 100644 index 000000000..415d156ae --- /dev/null +++ b/pkg/config/runtime_test.go @@ -0,0 +1,726 @@ +package config_test + +import ( + "testing" + + "github.com/containous/traefik/pkg/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// all the Routers/Middlewares/Services are considered fully qualified +func TestPopulateUsedby(t *testing.T) { + testCases := []struct { + desc string + conf *config.RuntimeConfiguration + expected config.RuntimeConfiguration + }{ + { + desc: "nil config", + conf: nil, + expected: config.RuntimeConfiguration{}, + }, + { + desc: "One service used by two routers", + conf: &config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.foo": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1:8085", + Weight: 1, + }, + { + URL: "http://127.0.0.1:8086", + Weight: 1, + }, + }, + Method: "wrr", + HealthCheck: &config.HealthCheck{ + Interval: "500ms", + Path: "/health", + }, + }, + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.foo": {}, + "myprovider.bar": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar", "myprovider.foo"}, + }, + }, + }, + }, + { + desc: "One service used by two routers, but one router with wrong rule", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.foo": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "WrongRule(`bar.foo`)", + }, + }, + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.foo": {}, + "myprovider.bar": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar", "myprovider.foo"}, + }, + }, + }, + }, + { + desc: "Broken Service used by one Router", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: nil, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + { + desc: "2 different Services each used by a disctinct router.", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1:8085", + Weight: 1, + }, + { + URL: "http://127.0.0.1:8086", + Weight: 1, + }, + }, + Method: "wrr", + HealthCheck: &config.HealthCheck{ + Interval: "500ms", + Path: "/health", + }, + }, + }, + }, + "myprovider.bar-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1:8087", + Weight: 1, + }, + { + URL: "http://127.0.0.1:8088", + Weight: 1, + }, + }, + Method: "wrr", + HealthCheck: &config.HealthCheck{ + Interval: "500ms", + Path: "/health", + }, + }, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.foo": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": {}, + "myprovider.foo": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.foo"}, + }, + "myprovider.bar-service": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + { + desc: "2 middlewares both used by 2 Routers", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + Middleware: &config.Middleware{ + BasicAuth: &config.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + }, + "myprovider.addPrefixTest": { + Middleware: &config.Middleware{ + AddPrefix: &config.AddPrefix{ + Prefix: "/toto", + }, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest"}, + }, + }, + "myprovider.test": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": {}, + "myprovider.test": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar", "myprovider.test"}, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + UsedBy: []string{"myprovider.bar", "myprovider.test"}, + }, + "myprovider.addPrefixTest": { + UsedBy: []string{"myprovider.bar", "myprovider.test"}, + }, + }, + }, + }, + { + desc: "Unknown middleware is not used by the Router", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + Middleware: &config.Middleware{ + BasicAuth: &config.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"unknown"}, + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + { + desc: "Broken middleware is used by Router", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + Middleware: &config.Middleware{ + BasicAuth: &config.BasicAuth{ + Users: []string{"badConf"}, + }, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"myprovider.auth"}, + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + { + desc: "2 middlewares from 2 disctinct providers both used by 2 Routers", + conf: &config.RuntimeConfiguration{ + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + Middleware: &config.Middleware{ + BasicAuth: &config.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + }, + "myprovider.addPrefixTest": { + Middleware: &config.Middleware{ + AddPrefix: &config.AddPrefix{ + Prefix: "/titi", + }, + }, + }, + "anotherprovider.addPrefixTest": { + Middleware: &config.Middleware{ + AddPrefix: &config.AddPrefix{ + Prefix: "/toto", + }, + }, + }, + }, + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "anotherprovider.addPrefixTest"}, + }, + }, + "myprovider.test": { + Router: &config.Router{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + Routers: map[string]*config.RouterInfo{ + "myprovider.bar": {}, + "myprovider.test": {}, + }, + Services: map[string]*config.ServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar", "myprovider.test"}, + }, + }, + Middlewares: map[string]*config.MiddlewareInfo{ + "myprovider.auth": { + UsedBy: []string{"myprovider.bar", "myprovider.test"}, + }, + "myprovider.addPrefixTest": { + UsedBy: []string{"myprovider.test"}, + }, + "anotherprovider.addPrefixTest": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + + // TCP tests from hereon + { + desc: "TCP, One service used by two routers", + conf: &config.RuntimeConfiguration{ + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + "myprovider.bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1", + Port: "8085", + Weight: 1, + }, + { + Address: "127.0.0.1", + Port: "8086", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.foo": {}, + "myprovider.bar": {}, + }, + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar", "myprovider.foo"}, + }, + }, + }, + }, + { + desc: "TCP, One service used by two routers, but one router with wrong rule", + conf: &config.RuntimeConfiguration{ + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "WrongRule(`bar.foo`)", + }, + }, + "myprovider.bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.foo": {}, + "myprovider.bar": {}, + }, + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar", "myprovider.foo"}, + }, + }, + }, + }, + { + desc: "TCP, Broken Service used by one Router", + conf: &config.RuntimeConfiguration{ + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: nil, + }, + }, + }, + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.bar": {}, + }, + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + { + desc: "TCP, 2 different Services each used by a disctinct router.", + conf: &config.RuntimeConfiguration{ + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1", + Port: "8085", + Weight: 1, + }, + { + Address: "127.0.0.1", + Port: "8086", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + "myprovider.bar-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1", + Port: "8087", + Weight: 1, + }, + { + Address: "127.0.0.1", + Port: "8088", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.foo-service", + Rule: "Host(`bar.foo`)", + }, + }, + "myprovider.bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "myprovider.bar-service", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: config.RuntimeConfiguration{ + TCPRouters: map[string]*config.TCPRouterInfo{ + "myprovider.bar": {}, + "myprovider.foo": {}, + }, + TCPServices: map[string]*config.TCPServiceInfo{ + "myprovider.foo-service": { + UsedBy: []string{"myprovider.foo"}, + }, + "myprovider.bar-service": { + UsedBy: []string{"myprovider.bar"}, + }, + }, + }, + }, + } + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + runtimeConf := test.conf + runtimeConf.PopulateUsedBy() + + for key, expectedService := range test.expected.Services { + require.NotNil(t, runtimeConf.Services[key]) + assert.Equal(t, expectedService.UsedBy, runtimeConf.Services[key].UsedBy) + } + + for key, expectedMiddleware := range test.expected.Middlewares { + require.NotNil(t, runtimeConf.Middlewares[key]) + assert.Equal(t, expectedMiddleware.UsedBy, runtimeConf.Middlewares[key].UsedBy) + } + + for key, expectedTCPService := range test.expected.TCPServices { + require.NotNil(t, runtimeConf.TCPServices[key]) + assert.Equal(t, expectedTCPService.UsedBy, runtimeConf.TCPServices[key].UsedBy) + } + }) + } + +} diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index 6313e35a2..11ccfab6f 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -10,12 +10,18 @@ import ( "sync" "time" + "github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/safe" "github.com/go-kit/kit/metrics" "github.com/vulcand/oxy/roundrobin" ) +const ( + serverUp = "UP" + serverDown = "DOWN" +) + var singleton *HealthCheck var once sync.Once @@ -221,3 +227,38 @@ func checkHealth(serverURL *url.URL, backend *BackendConfig) error { return nil } + +// NewLBStatusUpdater returns a new LbStatusUpdater +func NewLBStatusUpdater(bh BalancerHandler, svinfo *config.ServiceInfo) *LbStatusUpdater { + return &LbStatusUpdater{ + BalancerHandler: bh, + serviceInfo: svinfo, + } +} + +// LbStatusUpdater wraps a BalancerHandler and a ServiceInfo, +// so it can keep track of the status of a server in the ServiceInfo. +type LbStatusUpdater struct { + BalancerHandler + serviceInfo *config.ServiceInfo // can be nil +} + +// RemoveServer removes the given server from the BalancerHandler, +// and updates the status of the server to "DOWN". +func (lb *LbStatusUpdater) RemoveServer(u *url.URL) error { + err := lb.BalancerHandler.RemoveServer(u) + if err == nil && lb.serviceInfo != nil { + lb.serviceInfo.UpdateStatus(u.String(), serverDown) + } + return err +} + +// UpsertServer adds the given server to the BalancerHandler, +// and updates the status of the server to "UP". +func (lb *LbStatusUpdater) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error { + err := lb.BalancerHandler.UpsertServer(u, options...) + if err == nil && lb.serviceInfo != nil { + lb.serviceInfo.UpdateStatus(u.String(), serverUp) + } + return err +} diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index ff92d00d3..e60126b8c 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/testhelpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -367,6 +368,8 @@ type testLoadBalancer struct { numRemovedServers int numUpsertedServers int servers []*url.URL + // options is just to make sure that LBStatusUpdater forwards options on Upsert to its BalancerHandler + options []roundrobin.ServerOption } func (lb *testLoadBalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -386,6 +389,7 @@ func (lb *testLoadBalancer) UpsertServer(u *url.URL, options ...roundrobin.Serve defer lb.Unlock() lb.numUpsertedServers++ lb.servers = append(lb.servers, u) + lb.options = append(lb.options, options...) return nil } @@ -393,14 +397,23 @@ func (lb *testLoadBalancer) Servers() []*url.URL { return lb.servers } +func (lb *testLoadBalancer) Options() []roundrobin.ServerOption { + return lb.options +} + func (lb *testLoadBalancer) removeServer(u *url.URL) { var i int var serverURL *url.URL + found := false for i, serverURL = range lb.servers { if *serverURL == *u { + found = true break } } + if !found { + return + } lb.servers = append(lb.servers[:i], lb.servers[i+1:]...) } @@ -427,3 +440,32 @@ func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { th.done() } } + +func TestLBStatusUpdater(t *testing.T) { + lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}} + svInfo := &config.ServiceInfo{} + lbsu := NewLBStatusUpdater(lb, svInfo) + newServer, err := url.Parse("http://foo.com") + assert.Nil(t, err) + err = lbsu.UpsertServer(newServer, roundrobin.Weight(1)) + assert.Nil(t, err) + assert.Equal(t, len(lbsu.Servers()), 1) + assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1) + statuses := svInfo.GetAllStatus() + assert.Equal(t, len(statuses), 1) + for k, v := range statuses { + assert.Equal(t, k, newServer.String()) + assert.Equal(t, v, serverUp) + break + } + err = lbsu.RemoveServer(newServer) + assert.Nil(t, err) + assert.Equal(t, len(lbsu.Servers()), 0) + statuses = svInfo.GetAllStatus() + assert.Equal(t, len(statuses), 1) + for k, v := range statuses { + assert.Equal(t, k, newServer.String()) + assert.Equal(t, v, serverDown) + break + } +} diff --git a/pkg/responsemodifiers/response_modifier.go b/pkg/responsemodifiers/response_modifier.go index 264059e30..bd3199a18 100644 --- a/pkg/responsemodifiers/response_modifier.go +++ b/pkg/responsemodifiers/response_modifier.go @@ -8,13 +8,13 @@ import ( ) // NewBuilder creates a builder. -func NewBuilder(configs map[string]*config.Middleware) *Builder { +func NewBuilder(configs map[string]*config.MiddlewareInfo) *Builder { return &Builder{configs: configs} } // Builder holds builder configuration. type Builder struct { - configs map[string]*config.Middleware + configs map[string]*config.MiddlewareInfo } // Build Builds the response modifier. diff --git a/pkg/responsemodifiers/response_modifier_test.go b/pkg/responsemodifiers/response_modifier_test.go index e924238e4..f409d80ce 100644 --- a/pkg/responsemodifiers/response_modifier_test.go +++ b/pkg/responsemodifiers/response_modifier_test.go @@ -166,7 +166,12 @@ func TestBuilderBuild(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - builder := NewBuilder(test.conf) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Middlewares: test.conf, + }, + }) + builder := NewBuilder(rtConf.Middlewares) rm := builder.Build(context.Background(), test.middlewares) diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index c76e511e5..10cb83d46 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -39,7 +39,7 @@ const ( // Builder the middleware builder type Builder struct { - configs map[string]*config.Middleware + configs map[string]*config.MiddlewareInfo serviceBuilder serviceBuilder } @@ -48,7 +48,7 @@ type serviceBuilder interface { } // NewBuilder creates a new Builder -func NewBuilder(configs map[string]*config.Middleware, serviceBuilder serviceBuilder) *Builder { +func NewBuilder(configs map[string]*config.MiddlewareInfo, serviceBuilder serviceBuilder) *Builder { return &Builder{configs: configs, serviceBuilder: serviceBuilder} } @@ -60,20 +60,29 @@ func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.C chain = chain.Append(func(next http.Handler) (http.Handler, error) { constructorContext := internal.AddProviderInContext(ctx, middlewareName) - if _, ok := b.configs[middlewareName]; !ok { + if midInf, ok := b.configs[middlewareName]; !ok || midInf.Middleware == nil { return nil, fmt.Errorf("middleware %q does not exist", middlewareName) } var err error if constructorContext, err = checkRecursion(constructorContext, middlewareName); err != nil { + b.configs[middlewareName].Err = err return nil, err } - constructor, err := b.buildConstructor(constructorContext, middlewareName, *b.configs[middlewareName]) + constructor, err := b.buildConstructor(constructorContext, middlewareName) if err != nil { - return nil, fmt.Errorf("error during instanciation of %s: %v", middlewareName, err) + b.configs[middlewareName].Err = err + return nil, err } - return constructor(next) + + handler, err := constructor(next) + if err != nil { + b.configs[middlewareName].Err = err + return nil, err + } + + return handler, nil }) } return &chain @@ -90,7 +99,9 @@ func checkRecursion(ctx context.Context, middlewareName string) (context.Context return context.WithValue(ctx, middlewareStackKey, append(currentStack, middlewareName)), nil } -func (b *Builder) buildConstructor(ctx context.Context, middlewareName string, config config.Middleware) (alice.Constructor, error) { +// it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists +func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) { + config := b.configs[middlewareName] var middleware alice.Constructor badConf := errors.New("cannot create middleware: multi-types middleware not supported, consider declaring two different pieces of middleware instead") diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index ebab0a4fb..357666864 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -14,7 +14,7 @@ import ( ) func TestBuilder_BuildChainNilConfig(t *testing.T) { - testConfig := map[string]*config.Middleware{ + testConfig := map[string]*config.MiddlewareInfo{ "empty": {}, } middlewaresBuilder := NewBuilder(testConfig, nil) @@ -25,7 +25,7 @@ func TestBuilder_BuildChainNilConfig(t *testing.T) { } func TestBuilder_BuildChainNonExistentChain(t *testing.T) { - testConfig := map[string]*config.Middleware{ + testConfig := map[string]*config.MiddlewareInfo{ "foobar": {}, } middlewaresBuilder := NewBuilder(testConfig, nil) @@ -264,7 +264,12 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { ctx = internal.AddProviderInContext(ctx, test.contextProvider+".foobar") } - builder := NewBuilder(test.configuration, nil) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Middlewares: test.configuration, + }, + }) + builder := NewBuilder(rtConf.Middlewares, nil) result := builder.BuildChain(ctx, test.buildChain) @@ -310,7 +315,12 @@ func TestBuilder_buildConstructor(t *testing.T) { }, } - middlewaresBuilder := NewBuilder(testConfig, nil) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Middlewares: testConfig, + }, + }) + middlewaresBuilder := NewBuilder(rtConf.Middlewares, nil) testCases := []struct { desc string @@ -344,7 +354,8 @@ func TestBuilder_buildConstructor(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - constructor, err := middlewaresBuilder.buildConstructor(context.Background(), test.middlewareID, *testConfig[test.middlewareID]) + constructor, err := middlewaresBuilder.buildConstructor(context.Background(), test.middlewareID) + require.NoError(t, err) middleware, err2 := constructor(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) diff --git a/pkg/server/router/route_appender_aggregator.go b/pkg/server/router/route_appender_aggregator.go index 16260b63e..fcea9aec7 100644 --- a/pkg/server/router/route_appender_aggregator.go +++ b/pkg/server/router/route_appender_aggregator.go @@ -6,10 +6,10 @@ import ( "github.com/containous/alice" "github.com/containous/mux" "github.com/containous/traefik/pkg/api" + "github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/metrics" - "github.com/containous/traefik/pkg/safe" "github.com/containous/traefik/pkg/types" ) @@ -19,7 +19,8 @@ type chainBuilder interface { } // NewRouteAppenderAggregator Creates a new RouteAppenderAggregator -func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, conf static.Configuration, entryPointName string, currentConfiguration *safe.Safe) *RouteAppenderAggregator { +func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, conf static.Configuration, + entryPointName string, runtimeConfiguration *config.RuntimeConfiguration) *RouteAppenderAggregator { aggregator := &RouteAppenderAggregator{} if conf.Providers != nil && conf.Providers.Rest != nil { @@ -29,17 +30,9 @@ func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, if conf.API != nil && conf.API.EntryPoint == entryPointName { chain := chainBuilder.BuildChain(ctx, conf.API.Middlewares) aggregator.AddAppender(&WithMiddleware{ - appender: api.Handler{ - EntryPoint: conf.API.EntryPoint, - Dashboard: conf.API.Dashboard, - Statistics: conf.API.Statistics, - DashboardAssets: conf.API.DashboardAssets, - CurrentConfigurations: currentConfiguration, - Debug: conf.Global.Debug, - }, + appender: api.New(conf, runtimeConfiguration), routerMiddlewares: chain, }) - } if conf.Ping != nil && conf.Ping.EntryPoint == entryPointName { diff --git a/pkg/server/router/route_appender_aggregator_test.go b/pkg/server/router/route_appender_aggregator_test.go index 7722e0628..a9406ad39 100644 --- a/pkg/server/router/route_appender_aggregator_test.go +++ b/pkg/server/router/route_appender_aggregator_test.go @@ -62,7 +62,7 @@ func TestNewRouteAppenderAggregator(t *testing.T) { "/wrong": http.StatusBadGateway, "/ping": http.StatusOK, // "/.well-known/acme-challenge/token": http.StatusNotFound, // FIXME - "/api/providers": http.StatusUnauthorized, + "/api/rawdata": http.StatusUnauthorized, }, }, { diff --git a/pkg/server/router/route_appender_factory.go b/pkg/server/router/route_appender_factory.go index a22f36239..eec0b36b5 100644 --- a/pkg/server/router/route_appender_factory.go +++ b/pkg/server/router/route_appender_factory.go @@ -3,9 +3,9 @@ package router import ( "context" + "github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/provider/acme" - "github.com/containous/traefik/pkg/safe" "github.com/containous/traefik/pkg/server/middleware" "github.com/containous/traefik/pkg/types" ) @@ -27,8 +27,8 @@ type RouteAppenderFactory struct { } // NewAppender Creates a new RouteAppender -func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, currentConfiguration *safe.Safe) types.RouteAppender { - aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, currentConfiguration) +func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, runtimeConfiguration *config.RuntimeConfiguration) types.RouteAppender { + aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, runtimeConfiguration) if r.acmeProvider != nil && r.acmeProvider.HTTPChallenge != nil && r.acmeProvider.HTTPChallenge.EntryPoint == r.entryPointName { aggregator.AddAppender(r.acmeProvider) diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index e80afff34..e963ec9f2 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -23,7 +23,7 @@ const ( ) // NewManager Creates a new Manager -func NewManager(routers map[string]*config.Router, +func NewManager(routers map[string]*config.RouterInfo, serviceManager *service.Manager, middlewaresBuilder *middleware.Builder, modifierBuilder *responsemodifiers.Builder, ) *Manager { return &Manager{ @@ -38,7 +38,7 @@ func NewManager(routers map[string]*config.Router, // Manager A route/router manager type Manager struct { routerHandlers map[string]http.Handler - configs map[string]*config.Router + configs map[string]*config.RouterInfo serviceManager *service.Manager middlewaresBuilder *middleware.Builder modifierBuilder *responsemodifiers.Builder @@ -75,8 +75,17 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t return entryPointHandlers } -func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.Router { - entryPointsRouters := make(map[string]map[string]*config.Router) +func contains(entryPoints []string, entryPointName string) bool { + for _, name := range entryPoints { + if name == entryPointName { + return true + } + } + return false +} + +func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.RouterInfo { + entryPointsRouters := make(map[string]map[string]*config.RouterInfo) for rtName, rt := range m.configs { if (tls && rt.TLS == nil) || (!tls && rt.TLS != nil) { @@ -95,7 +104,7 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls } if _, ok := entryPointsRouters[entryPointName]; !ok { - entryPointsRouters[entryPointName] = make(map[string]*config.Router) + entryPointsRouters[entryPointName] = make(map[string]*config.RouterInfo) } entryPointsRouters[entryPointName][rtName] = rt @@ -105,7 +114,7 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls return entryPointsRouters } -func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.Router) (http.Handler, error) { +func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.RouterInfo) (http.Handler, error) { router, err := rules.NewRouter() if err != nil { return nil, err @@ -117,12 +126,14 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string handler, err := m.buildRouterHandler(ctxRouter, routerName) if err != nil { + routerConfig.Err = err.Error() logger.Error(err) continue } err = router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler) if err != nil { + routerConfig.Err = err.Error() logger.Error(err) continue } @@ -166,7 +177,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string) (ht return m.routerHandlers[routerName], nil } -func (m *Manager) buildHTTPHandler(ctx context.Context, router *config.Router, routerName string) (http.Handler, error) { +func (m *Manager) buildHTTPHandler(ctx context.Context, router *config.RouterInfo, routerName string) (http.Handler, error) { qualifiedNames := make([]string, len(router.Middlewares)) for i, name := range router.Middlewares { qualifiedNames[i] = internal.GetQualifiedName(ctx, name) @@ -186,12 +197,3 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *config.Router, r return alice.New().Extend(*mHandler).Append(tHandler).Then(sHandler) } - -func contains(entryPoints []string, entryPointName string) bool { - for _, name := range entryPoints { - if name == entryPointName { - return true - } - } - return false -} diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index c242b276b..b31476d1d 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -314,11 +314,17 @@ func TestRouterManager_Get(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport) - middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager) - responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig) - - routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Services: test.serviceConfig, + Routers: test.routersConfig, + Middlewares: test.middlewaresConfig, + }, + }) + serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) + routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false) @@ -413,11 +419,17 @@ func TestAccessLog(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport) - middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager) - responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig) - - routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Services: test.serviceConfig, + Routers: test.routersConfig, + Middlewares: test.middlewaresConfig, + }, + }) + serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) + routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false) @@ -443,6 +455,310 @@ func TestAccessLog(t *testing.T) { } } +func TestRuntimeConfiguration(t *testing.T) { + testCases := []struct { + desc string + serviceConfig map[string]*config.Service + routerConfig map[string]*config.Router + middlewareConfig map[string]*config.Middleware + expectedError int + }{ + { + desc: "No error", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1:8085", + Weight: 1, + }, + { + URL: "http://127.0.0.1:8086", + Weight: 1, + }, + }, + Method: "wrr", + HealthCheck: &config.HealthCheck{ + Interval: "500ms", + Path: "/health", + }, + }, + }, + }, + routerConfig: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + expectedError: 0, + }, + { + desc: "One router with wrong rule", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + routerConfig: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "WrongRule(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + expectedError: 1, + }, + { + desc: "All router with wrong rule", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + routerConfig: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "WrongRule(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "WrongRule(`foo.bar`)", + }, + }, + expectedError: 2, + }, + { + desc: "Router with unknown service", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + routerConfig: map[string]*config.Router{ + "foo": { + EntryPoints: []string{"web"}, + Service: "wrong-service", + Rule: "Host(`bar.foo`)", + }, + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + expectedError: 1, + }, + { + desc: "Router with broken service", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: nil, + }, + }, + routerConfig: map[string]*config.Router{ + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + }, + }, + expectedError: 2, + }, + { + desc: "Router with middleware", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + middlewareConfig: map[string]*config.Middleware{ + "auth": { + BasicAuth: &config.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + "addPrefixTest": { + AddPrefix: &config.AddPrefix{ + Prefix: "/toto", + }, + }, + }, + routerConfig: map[string]*config.Router{ + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest"}, + }, + "test": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + }, + }, + { + desc: "Router with unknown middleware", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + middlewareConfig: map[string]*config.Middleware{ + "auth": { + BasicAuth: &config.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + }, + routerConfig: map[string]*config.Router{ + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"unknown"}, + }, + }, + expectedError: 1, + }, + + { + desc: "Router with broken middleware", + serviceConfig: map[string]*config.Service{ + "foo-service": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "http://127.0.0.1", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + middlewareConfig: map[string]*config.Middleware{ + "auth": { + BasicAuth: &config.BasicAuth{ + Users: []string{"foo"}, + }, + }, + }, + routerConfig: map[string]*config.Router{ + "bar": { + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth"}, + }, + }, + expectedError: 2, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + entryPoints := []string{"web"} + + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Services: test.serviceConfig, + Routers: test.routerConfig, + Middlewares: test.middlewareConfig, + }, + }) + serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + responseModifierFactory := responsemodifiers.NewBuilder(map[string]*config.MiddlewareInfo{}) + routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) + + _ = routerManager.BuildHandlers(context.Background(), entryPoints, false) + + // even though rtConf was passed by argument to the manager builders above, + // it's ok to use it as the result we check, because everything worth checking + // can be accessed by pointers in it. + var allErrors int + for _, v := range rtConf.Services { + if v.Err != nil { + allErrors++ + } + } + for _, v := range rtConf.Routers { + if v.Err != "" { + allErrors++ + } + } + for _, v := range rtConf.Middlewares { + if v.Err != nil { + allErrors++ + } + } + assert.Equal(t, test.expectedError, allErrors) + }) + } + +} + type staticTransport struct { res *http.Response } @@ -480,11 +796,17 @@ func BenchmarkRouterServe(b *testing.B) { } entryPoints := []string{"web"} - serviceManager := service.NewManager(serviceConfig, &staticTransport{res}) - middlewaresBuilder := middleware.NewBuilder(map[string]*config.Middleware{}, serviceManager) - responseModifierFactory := responsemodifiers.NewBuilder(map[string]*config.Middleware{}) - - routerManager := NewManager(routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Services: serviceConfig, + Routers: routersConfig, + Middlewares: map[string]*config.Middleware{}, + }, + }) + serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) + routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory) handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false) @@ -498,6 +820,7 @@ func BenchmarkRouterServe(b *testing.B) { } } + func BenchmarkService(b *testing.B) { res := &http.Response{ StatusCode: 200, @@ -518,7 +841,12 @@ func BenchmarkService(b *testing.B) { }, } - serviceManager := service.NewManager(serviceConfig, &staticTransport{res}) + rtConf := config.NewRuntimeConfig(config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Services: serviceConfig, + }, + }) + serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}) 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 7479d159a..ba3e687f9 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -3,6 +3,7 @@ package tcp import ( "context" "crypto/tls" + "fmt" "net/http" "github.com/containous/traefik/pkg/config" @@ -14,14 +15,14 @@ import ( ) // NewManager Creates a new Manager -func NewManager(routers map[string]*config.TCPRouter, +func NewManager(conf *config.RuntimeConfiguration, serviceManager *tcpservice.Manager, httpHandlers map[string]http.Handler, httpsHandlers map[string]http.Handler, tlsConfig *tls.Config, ) *Manager { return &Manager{ - configs: routers, + configs: conf.TCPRouters, serviceManager: serviceManager, httpHandlers: httpHandlers, httpsHandlers: httpsHandlers, @@ -31,7 +32,7 @@ func NewManager(routers map[string]*config.TCPRouter, // Manager is a route/router manager type Manager struct { - configs map[string]*config.TCPRouter + configs map[string]*config.TCPRouterInfo serviceManager *tcpservice.Manager httpHandlers map[string]http.Handler httpsHandlers map[string]http.Handler @@ -60,7 +61,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m return entryPointHandlers } -func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.TCPRouter, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) { +func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.TCPRouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) { router := &tcp.Router{} router.HTTPHandler(handlerHTTP) router.HTTPSHandler(handlerHTTPS, m.tlsConfig) @@ -71,18 +72,21 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string handler, err := m.serviceManager.BuildTCP(ctxRouter, routerConfig.Service) if err != nil { + routerConfig.Err = err.Error() logger.Error(err) continue } domains, err := rules.ParseHostSNI(routerConfig.Rule) if err != nil { - logger.Debugf("Unknown rule %s", routerConfig.Rule) + routerErr := fmt.Errorf("unknown rule %s", routerConfig.Rule) + routerConfig.Err = routerErr.Error() + logger.Debug(routerErr) continue } for _, domain := range domains { - logger.Debugf("Add route %s on TCP", domain) + logger.Debugf("Adding route %s on TCP", domain) switch { case routerConfig.TLS != nil: if routerConfig.TLS.Passthrough { @@ -101,8 +105,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string return router, nil } -func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map[string]map[string]*config.TCPRouter { - entryPointsRouters := make(map[string]map[string]*config.TCPRouter) +func contains(entryPoints []string, entryPointName string) bool { + for _, name := range entryPoints { + if name == entryPointName { + return true + } + } + return false +} + +func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map[string]map[string]*config.TCPRouterInfo { + entryPointsRouters := make(map[string]map[string]*config.TCPRouterInfo) for rtName, rt := range m.configs { eps := rt.EntryPoints @@ -118,7 +131,7 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map } if _, ok := entryPointsRouters[entryPointName]; !ok { - entryPointsRouters[entryPointName] = make(map[string]*config.TCPRouter) + entryPointsRouters[entryPointName] = make(map[string]*config.TCPRouterInfo) } entryPointsRouters[entryPointName][rtName] = rt @@ -127,12 +140,3 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map return entryPointsRouters } - -func contains(entryPoints []string, entryPointName string) bool { - for _, name := range entryPoints { - if name == entryPointName { - return true - } - } - return false -} diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go new file mode 100644 index 000000000..c6543f008 --- /dev/null +++ b/pkg/server/router/tcp/router_test.go @@ -0,0 +1,223 @@ +package tcp + +import ( + "context" + "testing" + + "github.com/containous/traefik/pkg/config" + "github.com/containous/traefik/pkg/server/service/tcp" + "github.com/stretchr/testify/assert" +) + +func TestRuntimeConfiguration(t *testing.T) { + testCases := []struct { + desc string + serviceConfig map[string]*config.TCPServiceInfo + routerConfig map[string]*config.TCPRouterInfo + expectedError int + }{ + { + desc: "No error", + serviceConfig: map[string]*config.TCPServiceInfo{ + "foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Port: "8085", + Address: "127.0.0.1:8085", + }, + { + Address: "127.0.0.1:8086", + Port: "8086", + }, + }, + Method: "wrr", + }, + }, + }, + }, + routerConfig: map[string]*config.TCPRouterInfo{ + "foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "HostSNI(`bar.foo`)", + }, + }, + "bar": { + TCPRouter: &config.TCPRouter{ + + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "HostSNI(`foo.bar`)", + }, + }, + }, + expectedError: 0, + }, + { + desc: "One router with wrong rule", + serviceConfig: map[string]*config.TCPServiceInfo{ + "foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1:80", + }, + }, + Method: "wrr", + }, + }, + }, + }, + routerConfig: map[string]*config.TCPRouterInfo{ + "foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "WrongRule(`bar.foo`)", + }, + }, + + "bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "HostSNI(`foo.bar`)", + }, + }, + }, + expectedError: 1, + }, + { + desc: "All router with wrong rule", + serviceConfig: map[string]*config.TCPServiceInfo{ + "foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1:80", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + routerConfig: map[string]*config.TCPRouterInfo{ + "foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "WrongRule(`bar.foo`)", + }, + }, + "bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "WrongRule(`foo.bar`)", + }, + }, + }, + expectedError: 2, + }, + { + desc: "Router with unknown service", + serviceConfig: map[string]*config.TCPServiceInfo{ + "foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "127.0.0.1:80", + Weight: 1, + }, + }, + Method: "wrr", + }, + }, + }, + }, + routerConfig: map[string]*config.TCPRouterInfo{ + "foo": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "wrong-service", + Rule: "HostSNI(`bar.foo`)", + }, + }, + "bar": { + TCPRouter: &config.TCPRouter{ + + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "HostSNI(`foo.bar`)", + }, + }, + }, + expectedError: 1, + }, + { + desc: "Router with broken service", + serviceConfig: map[string]*config.TCPServiceInfo{ + "foo-service": { + TCPService: &config.TCPService{ + LoadBalancer: nil, + }, + }, + }, + routerConfig: map[string]*config.TCPRouterInfo{ + "bar": { + TCPRouter: &config.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "HostSNI(`foo.bar`)", + }, + }, + }, + expectedError: 2, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + entryPoints := []string{"web"} + + conf := &config.RuntimeConfiguration{ + TCPServices: test.serviceConfig, + TCPRouters: test.routerConfig, + } + serviceManager := tcp.NewManager(conf) + routerManager := NewManager(conf, serviceManager, + nil, nil, nil) + + _ = routerManager.BuildHandlers(context.Background(), entryPoints) + + // even though conf was passed by argument to the manager builders above, + // it's ok to use it as the result we check, because everything worth checking + // can be accessed by pointers in it. + var allErrors int + for _, v := range conf.TCPServices { + if v.Err != nil { + allErrors++ + } + } + for _, v := range conf.TCPRouters { + if v.Err != "" { + allErrors++ + } + } + assert.Equal(t, test.expectedError, allErrors) + }) + } + +} diff --git a/pkg/server/server.go b/pkg/server/server.go index e0e62dbf3..4a58b5570 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -50,7 +50,7 @@ type Server struct { // RouteAppenderFactory the route appender factory interface type RouteAppenderFactory interface { - NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, currentConfigurations *safe.Safe) types.RouteAppender + NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, runtimeConfiguration *config.RuntimeConfiguration) types.RouteAppender } func setupTracing(conf *static.Tracing) tracing.TrackingBackend { diff --git a/pkg/server/server_configuration.go b/pkg/server/server_configuration.go index 915fb3b1d..a5f5bab5f 100644 --- a/pkg/server/server_configuration.go +++ b/pkg/server/server_configuration.go @@ -69,47 +69,46 @@ func (s *Server) loadConfigurationTCP(configurations config.Configurations) map[ s.tlsManager.UpdateConfigs(conf.TLSStores, conf.TLSOptions, conf.TLS) - handlersNonTLS, handlersTLS := s.createHTTPHandlers(ctx, *conf.HTTP, entryPoints) - - routersTCP := s.createTCPRouters(ctx, conf.TCP, entryPoints, handlersNonTLS, handlersTLS, s.tlsManager.Get("default", "default")) + rtConf := config.NewRuntimeConfig(conf) + handlersNonTLS, handlersTLS := s.createHTTPHandlers(ctx, rtConf, entryPoints) + routersTCP := s.createTCPRouters(ctx, rtConf, entryPoints, handlersNonTLS, handlersTLS, s.tlsManager.Get("default", "default")) + rtConf.PopulateUsedBy() return routersTCP } -func (s *Server) createTCPRouters(ctx context.Context, configuration *config.TCPConfiguration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler, tlsConfig *tls.Config) map[string]*tcpCore.Router { +// the given configuration must not be nil. its fields will get mutated. +func (s *Server) createTCPRouters(ctx context.Context, configuration *config.RuntimeConfiguration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler, tlsConfig *tls.Config) map[string]*tcpCore.Router { if configuration == nil { return make(map[string]*tcpCore.Router) } - serviceManager := tcp.NewManager(configuration.Services) - routerManager := routertcp.NewManager(configuration.Routers, serviceManager, handlers, handlersTLS, tlsConfig) + serviceManager := tcp.NewManager(configuration) + routerManager := routertcp.NewManager(configuration, serviceManager, handlers, handlersTLS, tlsConfig) return routerManager.BuildHandlers(ctx, entryPoints) - } -func (s *Server) createHTTPHandlers(ctx context.Context, configuration config.HTTPConfiguration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) { +// 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 *config.RuntimeConfiguration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) { serviceManager := service.NewManager(configuration.Services, s.defaultRoundTripper) middlewaresBuilder := middleware.NewBuilder(configuration.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(configuration.Middlewares) - routerManager := router.NewManager(configuration.Routers, 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) + 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, middlewaresBuilder, &s.currentConfigurations) + appender := factory.NewAppender(ctx, middlewaresBuilder, configuration) appender.Append(internalMuxRouter) } diff --git a/pkg/server/server_configuration_test.go b/pkg/server/server_configuration_test.go index 2e9e76c38..4c4d50008 100644 --- a/pkg/server/server_configuration_test.go +++ b/pkg/server/server_configuration_test.go @@ -47,7 +47,8 @@ func TestReuseService(t *testing.T) { srv := NewServer(staticConfig, nil, entryPoints, nil) - entrypointsHandlers, _ := srv.createHTTPHandlers(context.Background(), *dynamicConfigs, []string{"http"}) + rtConf := config.NewRuntimeConfig(config.Configuration{HTTP: dynamicConfigs}) + entrypointsHandlers, _ := srv.createHTTPHandlers(context.Background(), rtConf, []string{"http"}) // Test that the /ok path returns a status 200. responseRecorderOk := &httptest.ResponseRecorder{} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index a1c538ab3..634e349f0 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -258,7 +258,8 @@ func TestServerResponseEmptyBackend(t *testing.T) { } srv := NewServer(globalConfig, nil, entryPointsConfig, nil) - entryPoints, _ := srv.createHTTPHandlers(context.Background(), *test.config(testServer.URL), []string{"http"}) + rtConf := config.NewRuntimeConfig(config.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) diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index da628a3d0..7383f517d 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -26,7 +26,7 @@ const ( ) // NewManager creates a new Manager -func NewManager(configs map[string]*config.Service, defaultRoundTripper http.RoundTripper) *Manager { +func NewManager(configs map[string]*config.ServiceInfo, defaultRoundTripper http.RoundTripper) *Manager { return &Manager{ bufferPool: newBufferPool(), defaultRoundTripper: defaultRoundTripper, @@ -40,7 +40,7 @@ type Manager struct { bufferPool httputil.BufferPool defaultRoundTripper http.RoundTripper balancers map[string][]healthcheck.BalancerHandler - configs map[string]*config.Service + configs map[string]*config.ServiceInfo } // BuildHTTP Creates a http.Handler for a service configuration. @@ -50,15 +50,25 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, respons serviceName = internal.GetQualifiedName(ctx, serviceName) ctx = internal.AddProviderInContext(ctx, serviceName) - if conf, ok := m.configs[serviceName]; ok { - // TODO Should handle multiple service types - // FIXME Check if the service is declared multiple times with different types - if conf.LoadBalancer != nil { - return m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier) - } - return nil, fmt.Errorf("the service %q doesn't have any load balancer", serviceName) + conf, ok := m.configs[serviceName] + if !ok { + return nil, fmt.Errorf("the service %q does not exist", serviceName) } - return nil, fmt.Errorf("the service %q does not exits", serviceName) + + // TODO Should handle multiple service types + // FIXME Check if the service is declared multiple times with different types + if conf.LoadBalancer == nil { + conf.Err = fmt.Errorf("the service %q doesn't have any load balancer", serviceName) + return nil, conf.Err + } + + lb, err := m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier) + if err != nil { + conf.Err = err + return nil, err + } + + return lb, nil } func (m *Manager) getLoadBalancerServiceHandler( @@ -158,7 +168,7 @@ func buildHealthCheckOptions(ctx context.Context, lb healthcheck.BalancerHandler } if timeout >= interval { - logger.Warnf("Health check timeout for backend '%s' should be lower than the health check interval. Interval set to timeout + 1 second (%s).", backend) + logger.Warnf("Health check timeout for backend '%s' should be lower than the health check interval. Interval set to timeout + 1 second (%s).", backend, interval) } return &healthcheck.Options{ @@ -229,7 +239,8 @@ func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, servi } } - if err := m.upsertServers(ctx, lb, service.Servers); err != nil { + lbsu := healthcheck.NewLBStatusUpdater(lb, m.configs[serviceName]) + if err := m.upsertServers(ctx, lbsu, service.Servers); err != nil { return nil, fmt.Errorf("error configuring load balancer for service %s: %v", serviceName, err) } diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index d066fbee2..cf33471b3 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -275,33 +275,39 @@ func TestManager_Build(t *testing.T) { testCases := []struct { desc string serviceName string - configs map[string]*config.Service + configs map[string]*config.ServiceInfo providerName string }{ { desc: "Simple service name", serviceName: "serviceName", - configs: map[string]*config.Service{ + configs: map[string]*config.ServiceInfo{ "serviceName": { - LoadBalancer: &config.LoadBalancerService{Method: "wrr"}, + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{Method: "wrr"}, + }, }, }, }, { desc: "Service name with provider", serviceName: "provider-1.serviceName", - configs: map[string]*config.Service{ + configs: map[string]*config.ServiceInfo{ "provider-1.serviceName": { - LoadBalancer: &config.LoadBalancerService{Method: "wrr"}, + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{Method: "wrr"}, + }, }, }, }, { desc: "Service name with provider in context", serviceName: "serviceName", - configs: map[string]*config.Service{ + configs: map[string]*config.ServiceInfo{ "provider-1.serviceName": { - LoadBalancer: &config.LoadBalancerService{Method: "wrr"}, + Service: &config.Service{ + LoadBalancer: &config.LoadBalancerService{Method: "wrr"}, + }, }, }, providerName: "provider-1", diff --git a/pkg/server/service/tcp/service.go b/pkg/server/service/tcp/service.go index 1bee3f08f..39880c718 100644 --- a/pkg/server/service/tcp/service.go +++ b/pkg/server/service/tcp/service.go @@ -13,13 +13,13 @@ import ( // Manager is the TCPHandlers factory type Manager struct { - configs map[string]*config.TCPService + configs map[string]*config.TCPServiceInfo } // NewManager creates a new manager -func NewManager(configs map[string]*config.TCPService) *Manager { +func NewManager(conf *config.RuntimeConfiguration) *Manager { return &Manager{ - configs: configs, + configs: conf.TCPServices, } } @@ -29,23 +29,23 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han ctx := internal.AddProviderInContext(rootCtx, serviceQualifiedName) ctx = log.With(ctx, log.Str(log.ServiceName, serviceName)) + // FIXME Check if the service is declared multiple times with different types conf, ok := m.configs[serviceQualifiedName] if !ok { - return nil, fmt.Errorf("the service %q does not exits", serviceQualifiedName) + return nil, fmt.Errorf("the service %q does not exist", serviceQualifiedName) } - if conf.LoadBalancer == nil { - return nil, fmt.Errorf("the service %q doesn't have any TCP load balancer", serviceQualifiedName) + conf.Err = fmt.Errorf("the service %q doesn't have any TCP load balancer", serviceQualifiedName) + return nil, conf.Err } logger := log.FromContext(ctx) - // FIXME Check if the service is declared multiple times with different types loadBalancer := tcp.NewRRLoadBalancer() for _, server := range conf.LoadBalancer.Servers { - if _, err := parseIP(server.Address); err != nil { - logger.Errorf("Invalid IP address for a %q server %q: %v", serviceQualifiedName, server.Address, err) + if _, _, err := net.SplitHostPort(server.Address); err != nil { + logger.Errorf("In service %q: %v", serviceQualifiedName, err) continue } @@ -57,20 +57,5 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han loadBalancer.AddServer(handler) } - return loadBalancer, nil } - -func parseIP(s string) (string, error) { - ip, _, err := net.SplitHostPort(s) - if err == nil { - return ip, nil - } - - ipNoPort := net.ParseIP(s) - if ipNoPort == nil { - return "", fmt.Errorf("invalid IP Address %s", ipNoPort) - } - - return ipNoPort.String(), nil -} diff --git a/pkg/server/service/tcp/service_test.go b/pkg/server/service/tcp/service_test.go index ee5337315..9693a1d74 100644 --- a/pkg/server/service/tcp/service_test.go +++ b/pkg/server/service/tcp/service_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/containous/traefik/pkg/config" + "github.com/containous/traefik/pkg/server/internal" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -12,49 +14,166 @@ func TestManager_BuildTCP(t *testing.T) { testCases := []struct { desc string serviceName string - configs map[string]*config.TCPService + configs map[string]*config.TCPServiceInfo + providerName string expectedError string }{ { desc: "without configuration", serviceName: "test", configs: nil, - expectedError: `the service "test" does not exits`, + expectedError: `the service "test" does not exist`, }, { desc: "missing lb configuration", serviceName: "test", - configs: map[string]*config.TCPService{ - "test": {}, + configs: map[string]*config.TCPServiceInfo{ + "test": { + TCPService: &config.TCPService{}, + }, }, expectedError: `the service "test" doesn't have any TCP load balancer`, }, { - desc: "no such host", + desc: "no such host, server is skipped, error is logged", serviceName: "test", - configs: map[string]*config.TCPService{ + configs: map[string]*config.TCPServiceInfo{ "test": { - LoadBalancer: &config.TCPLoadBalancerService{ - Servers: []config.TCPServer{ - {Address: "test:31"}, + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + {Address: "test:31"}, + }, }, }, }, }, }, { - desc: "invalid IP address", + desc: "invalid IP address, server is skipped, error is logged", serviceName: "test", - configs: map[string]*config.TCPService{ + configs: map[string]*config.TCPServiceInfo{ "test": { - LoadBalancer: &config.TCPLoadBalancerService{ - Servers: []config.TCPServer{ - {Address: "foobar"}, + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + {Address: "foobar"}, + }, }, }, }, }, }, + { + desc: "Simple service name", + serviceName: "serviceName", + configs: map[string]*config.TCPServiceInfo{ + "serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{Method: "wrr"}, + }, + }, + }, + }, + { + desc: "Service name with provider", + serviceName: "provider-1.serviceName", + configs: map[string]*config.TCPServiceInfo{ + "provider-1.serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{Method: "wrr"}, + }, + }, + }, + }, + { + desc: "Service name with provider in context", + serviceName: "serviceName", + configs: map[string]*config.TCPServiceInfo{ + "provider-1.serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{Method: "wrr"}, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "Server with correct host:port as address", + serviceName: "serviceName", + configs: map[string]*config.TCPServiceInfo{ + "provider-1.serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "foobar.com:80", + }, + }, + Method: "wrr", + }, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "Server with correct ip:port as address", + serviceName: "serviceName", + configs: map[string]*config.TCPServiceInfo{ + "provider-1.serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "192.168.0.12:80", + }, + }, + Method: "wrr", + }, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "missing port in address with hostname, server is skipped, error is logged", + serviceName: "serviceName", + configs: map[string]*config.TCPServiceInfo{ + "provider-1.serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "foobar.com", + }, + }, + Method: "wrr", + }, + }, + }, + }, + providerName: "provider-1", + }, + { + desc: "missing port in address with ip, server is skipped, error is logged", + serviceName: "serviceName", + configs: map[string]*config.TCPServiceInfo{ + "provider-1.serviceName": { + TCPService: &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "192.168.0.12", + }, + }, + Method: "wrr", + }, + }, + }, + }, + providerName: "provider-1", + }, } for _, test := range testCases { @@ -62,19 +181,22 @@ func TestManager_BuildTCP(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - manager := NewManager(test.configs) + manager := NewManager(&config.RuntimeConfiguration{ + TCPServices: test.configs, + }) - handler, err := manager.BuildTCP(context.Background(), test.serviceName) + ctx := context.Background() + if len(test.providerName) > 0 { + ctx = internal.AddProviderInContext(ctx, test.providerName+".foobar") + } + + handler, err := manager.BuildTCP(ctx, test.serviceName) if test.expectedError != "" { - if err == nil { - require.Error(t, err) - } else { - require.EqualError(t, err, test.expectedError) - require.Nil(t, handler) - } + assert.EqualError(t, err, test.expectedError) + require.Nil(t, handler) } else { - require.NoError(t, err) + assert.Nil(t, err) require.NotNil(t, handler) } }) diff --git a/vendor/github.com/thoas/stats/LICENSE b/vendor/github.com/thoas/stats/LICENSE deleted file mode 100644 index 7408e8e29..000000000 --- a/vendor/github.com/thoas/stats/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Florent Messa - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/vendor/github.com/thoas/stats/options.go b/vendor/github.com/thoas/stats/options.go deleted file mode 100644 index 5db124f24..000000000 --- a/vendor/github.com/thoas/stats/options.go +++ /dev/null @@ -1,59 +0,0 @@ -package stats - -// Options are stats options. -type Options struct { - statusCode *int - size int - recorder ResponseWriter -} - -// StatusCode returns the response status code. -func (o Options) StatusCode() int { - if o.recorder != nil { - return o.recorder.Status() - } - - return *o.statusCode -} - -// Size returns the response size. -func (o Options) Size() int { - if o.recorder != nil { - return o.recorder.Size() - } - - return o.size -} - -// Option represents a stats option. -type Option func(*Options) - -// WithStatusCode sets the status code to use in stats. -func WithStatusCode(statusCode int) Option { - return func(o *Options) { - o.statusCode = &statusCode - } -} - -// WithSize sets the size to use in stats. -func WithSize(size int) Option { - return func(o *Options) { - o.size = size - } -} - -// WithRecorder sets the recorder to use in stats. -func WithRecorder(recorder ResponseWriter) Option { - return func(o *Options) { - o.recorder = recorder - } -} - -// newOptions takes functional options and returns options. -func newOptions(options ...Option) *Options { - opts := &Options{} - for _, o := range options { - o(opts) - } - return opts -} diff --git a/vendor/github.com/thoas/stats/recorder.go b/vendor/github.com/thoas/stats/recorder.go deleted file mode 100644 index 326042c24..000000000 --- a/vendor/github.com/thoas/stats/recorder.go +++ /dev/null @@ -1,95 +0,0 @@ -package stats - -import ( - "bufio" - "fmt" - "net" - "net/http" -) - -type ResponseWriter interface { - http.ResponseWriter - http.Flusher - // Status returns the status code of the response or 0 if the response has not been written. - Status() int - // Written returns whether or not the ResponseWriter has been written. - Written() bool - // Size returns the size of the response body. - Size() int - // Before allows for a function to be called before the ResponseWriter has been written to. This is - // useful for setting headers or any other operations that must happen before a response has been written. - Before(func(ResponseWriter)) -} - -type beforeFunc func(ResponseWriter) - -type recorderResponseWriter struct { - http.ResponseWriter - status int - size int - beforeFuncs []beforeFunc - written bool -} - -func NewRecorderResponseWriter(w http.ResponseWriter, statusCode int) ResponseWriter { - return &recorderResponseWriter{ResponseWriter: w, status: statusCode} -} - -func (r *recorderResponseWriter) WriteHeader(code int) { - r.written = true - r.ResponseWriter.WriteHeader(code) - r.status = code -} - -func (r *recorderResponseWriter) Flush() { - flusher, ok := r.ResponseWriter.(http.Flusher) - if ok { - flusher.Flush() - } -} - -func (r *recorderResponseWriter) Status() int { - return r.status -} - -func (r *recorderResponseWriter) Write(b []byte) (int, error) { - if !r.Written() { - // The status will be StatusOK if WriteHeader has not been called yet - r.WriteHeader(http.StatusOK) - } - size, err := r.ResponseWriter.Write(b) - r.size += size - return size, err -} - -// Proxy method to Status to add support for gocraft -func (r *recorderResponseWriter) StatusCode() int { - return r.Status() -} - -func (r *recorderResponseWriter) Size() int { - return r.size -} - -func (r *recorderResponseWriter) Written() bool { - return r.StatusCode() != 0 -} - -func (r *recorderResponseWriter) CloseNotify() <-chan bool { - return r.ResponseWriter.(http.CloseNotifier).CloseNotify() -} - -func (r *recorderResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if !r.written { - r.status = 0 - } - hijacker, ok := r.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") - } - return hijacker.Hijack() -} - -func (r *recorderResponseWriter) Before(before func(ResponseWriter)) { - r.beforeFuncs = append(r.beforeFuncs, before) -} diff --git a/vendor/github.com/thoas/stats/stats.go b/vendor/github.com/thoas/stats/stats.go deleted file mode 100644 index 65dc2f99c..000000000 --- a/vendor/github.com/thoas/stats/stats.go +++ /dev/null @@ -1,227 +0,0 @@ -package stats - -import ( - "fmt" - "net/http" - "os" - "sync" - "time" -) - -// Stats data structure -type Stats struct { - mu sync.RWMutex - closed chan struct{} - Hostname string - Uptime time.Time - Pid int - ResponseCounts map[string]int - TotalResponseCounts map[string]int - TotalResponseTime time.Time - TotalResponseSize int64 - MetricsCounts map[string]int - MetricsTimers map[string]time.Time -} - -// Label data structure -type Label struct { - Name string - Value string -} - -// New constructs a new Stats structure -func New() *Stats { - name, _ := os.Hostname() - - stats := &Stats{ - closed: make(chan struct{}, 1), - Uptime: time.Now(), - Pid: os.Getpid(), - ResponseCounts: map[string]int{}, - TotalResponseCounts: map[string]int{}, - TotalResponseTime: time.Time{}, - Hostname: name, - } - - go func() { - for { - select { - case <-stats.closed: - return - default: - stats.ResetResponseCounts() - - time.Sleep(time.Second * 1) - } - } - }() - - return stats -} - -func (mw *Stats) Close() { - close(mw.closed) -} - -// ResetResponseCounts reset the response counts -func (mw *Stats) ResetResponseCounts() { - mw.mu.Lock() - defer mw.mu.Unlock() - mw.ResponseCounts = map[string]int{} -} - -// Handler is a MiddlewareFunc makes Stats implement the Middleware interface. -func (mw *Stats) Handler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - beginning, recorder := mw.Begin(w) - - h.ServeHTTP(recorder, r) - - mw.End(beginning, WithRecorder(recorder)) - }) -} - -// Negroni compatible interface -func (mw *Stats) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - beginning, recorder := mw.Begin(w) - - next(recorder, r) - - mw.End(beginning, WithRecorder(recorder)) -} - -// Begin starts a recorder -func (mw *Stats) Begin(w http.ResponseWriter) (time.Time, ResponseWriter) { - start := time.Now() - - writer := NewRecorderResponseWriter(w, 200) - - return start, writer -} - -// End closes the recorder with a specific status -func (mw *Stats) End(start time.Time, opts ...Option) { - options := newOptions(opts...) - - responseTime := time.Since(start) - - mw.mu.Lock() - - defer mw.mu.Unlock() - - // If Hijacked connection do not count in response time - if options.StatusCode() != 0 { - statusCode := fmt.Sprintf("%d", options.StatusCode()) - mw.ResponseCounts[statusCode]++ - mw.TotalResponseCounts[statusCode]++ - mw.TotalResponseTime = mw.TotalResponseTime.Add(responseTime) - mw.TotalResponseSize += int64(options.Size()) - } -} - -// MeasureSince method for execution time recording -func (mw *Stats) MeasureSince(key string, start time.Time) { - mw.MeasureSinceWithLabels(key, start, nil) -} - -// MeasureSinceWithLabels method for execution time recording with custom labels -func (mw *Stats) MeasureSinceWithLabels(key string, start time.Time, labels []Label) { - labels = append(labels, Label{"host", mw.Hostname}) - elapsed := time.Since(start) - - mw.mu.Lock() - defer mw.mu.Unlock() - - mw.MetricsCounts[key]++ - mw.MetricsTimers[key] = mw.MetricsTimers[key].Add(elapsed) -} - -// Data serializable structure -type Data struct { - Pid int `json:"pid"` - Hostname string `json:"hostname"` - UpTime string `json:"uptime"` - UpTimeSec float64 `json:"uptime_sec"` - Time string `json:"time"` - TimeUnix int64 `json:"unixtime"` - StatusCodeCount map[string]int `json:"status_code_count"` - TotalStatusCodeCount map[string]int `json:"total_status_code_count"` - Count int `json:"count"` - TotalCount int `json:"total_count"` - TotalResponseTime string `json:"total_response_time"` - TotalResponseTimeSec float64 `json:"total_response_time_sec"` - TotalResponseSize int64 `json:"total_response_size"` - AverageResponseSize int64 `json:"average_response_size"` - AverageResponseTime string `json:"average_response_time"` - AverageResponseTimeSec float64 `json:"average_response_time_sec"` - TotalMetricsCounts map[string]int `json:"total_metrics_counts"` - AverageMetricsTimers map[string]float64 `json:"average_metrics_timers"` -} - -// Data returns the data serializable structure -func (mw *Stats) Data() *Data { - mw.mu.RLock() - - responseCounts := make(map[string]int, len(mw.ResponseCounts)) - totalResponseCounts := make(map[string]int, len(mw.TotalResponseCounts)) - totalMetricsCounts := make(map[string]int, len(mw.MetricsCounts)) - metricsCounts := make(map[string]float64, len(mw.MetricsCounts)) - - now := time.Now() - - uptime := now.Sub(mw.Uptime) - - count := 0 - for code, current := range mw.ResponseCounts { - responseCounts[code] = current - count += current - } - - totalCount := 0 - for code, count := range mw.TotalResponseCounts { - totalResponseCounts[code] = count - totalCount += count - } - - totalResponseTime := mw.TotalResponseTime.Sub(time.Time{}) - totalResponseSize := mw.TotalResponseSize - - averageResponseTime := time.Duration(0) - averageResponseSize := int64(0) - if totalCount > 0 { - avgNs := int64(totalResponseTime) / int64(totalCount) - averageResponseTime = time.Duration(avgNs) - averageResponseSize = int64(totalResponseSize) / int64(totalCount) - } - - for key, count := range mw.MetricsCounts { - totalMetric := mw.MetricsTimers[key].Sub(time.Time{}) - avgNs := int64(totalMetric) / int64(count) - metricsCounts[key] = time.Duration(avgNs).Seconds() - totalMetricsCounts[key] = count - } - - mw.mu.RUnlock() - - r := &Data{ - Pid: mw.Pid, - UpTime: uptime.String(), - UpTimeSec: uptime.Seconds(), - Time: now.String(), - TimeUnix: now.Unix(), - StatusCodeCount: responseCounts, - TotalStatusCodeCount: totalResponseCounts, - Count: count, - TotalCount: totalCount, - TotalResponseTime: totalResponseTime.String(), - TotalResponseSize: totalResponseSize, - TotalResponseTimeSec: totalResponseTime.Seconds(), - TotalMetricsCounts: totalMetricsCounts, - AverageResponseSize: averageResponseSize, - AverageResponseTime: averageResponseTime.String(), - AverageResponseTimeSec: averageResponseTime.Seconds(), - AverageMetricsTimers: metricsCounts, - } - - return r -}