diff --git a/contrib/grafana/traefik-kubernetes.json b/contrib/grafana/traefik-kubernetes.json index ed7deaac4..4e463b502 100644 --- a/contrib/grafana/traefik-kubernetes.json +++ b/contrib/grafana/traefik-kubernetes.json @@ -1545,7 +1545,8 @@ "hide": 0, "includeAll": false, "multi": false, - "name": "datasource", + "name": "DS_PROMETHEUS", + "label": "datasource", "options": [], "query": "prometheus", "refresh": 1, diff --git a/contrib/grafana/traefik.json b/contrib/grafana/traefik.json index e4dbf5220..5c3d140ee 100644 --- a/contrib/grafana/traefik.json +++ b/contrib/grafana/traefik.json @@ -1537,7 +1537,8 @@ "hide": 0, "includeAll": false, "multi": false, - "name": "datasource", + "name": "DS_PROMETHEUS", + "label": "datasource", "options": [], "query": "prometheus", "refresh": 1, diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index d3fc20997..7d942b1a2 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -11,7 +11,11 @@ Automatic HTTPS You can configure Traefik to use an ACME provider (like Let's Encrypt) for automatic certificate generation. !!! warning "Let's Encrypt and Rate Limiting" - Note that Let's Encrypt API has [rate limiting](https://letsencrypt.org/docs/rate-limits). + Note that Let's Encrypt API has [rate limiting](https://letsencrypt.org/docs/rate-limits). These last up to __one week__, and can not be overridden. + + When running Traefik in a container this file should be persisted across restarts. + If Traefik requests new certificates each time it starts up, a crash-looping container can quickly reach Let's Encrypt's ratelimits. + To configure where certificates are stored, please take a look at the [storage](#storage) configuration. Use Let's Encrypt staging server with the [`caServer`](#caserver) configuration option when experimenting to avoid hitting this limit too fast. diff --git a/docs/content/observability/metrics/overview.md b/docs/content/observability/metrics/overview.md index 2a3e1d9c7..60d4e7d78 100644 --- a/docs/content/observability/metrics/overview.md +++ b/docs/content/observability/metrics/overview.md @@ -47,6 +47,12 @@ traefik.tls.certs.notAfterTimestamp {prefix}.tls.certs.notAfterTimestamp ``` +```opentelemetry tab="OpenTelemetry" +traefik_config_reloads_total +traefik_config_last_reload_success +traefik_tls_certs_not_after +``` + ## EntryPoint Metrics | Metric | Type | [Labels](#labels) | Description | @@ -95,6 +101,15 @@ traefik.entrypoint.responses.bytes.total {prefix}.entrypoint.responses.bytes.total ``` +```opentelemetry tab="OpenTelemetry" +traefik_entrypoint_requests_total +traefik_entrypoint_requests_tls_total +traefik_entrypoint_request_duration_seconds +traefik_entrypoint_open_connections +traefik_entrypoint_requests_bytes_total +traefik_entrypoint_responses_bytes_total +``` + ## Router Metrics | Metric | Type | [Labels](#labels) | Description | @@ -143,6 +158,15 @@ traefik.router.responses.bytes.total {prefix}.router.responses.bytes.total ``` +```opentelemetry tab="OpenTelemetry" +traefik_router_requests_total +traefik_router_requests_tls_total +traefik_router_request_duration_seconds +traefik_router_open_connections +traefik_router_requests_bytes_total +traefik_router_responses_bytes_total +``` + ## Service Metrics | Metric | Type | Labels | Description | @@ -201,6 +225,17 @@ traefik.service.responses.bytes.total {prefix}.service.responses.bytes.total ``` +```opentelemetry tab="OpenTelemetry" +traefik_service_requests_total +traefik_service_requests_tls_total +traefik_service_request_duration_seconds +traefik_service_open_connections +traefik_service_retries_total +traefik_service_server_up +traefik_service_requests_bytes_total +traefik_service_responses_bytes_total +``` + ## Labels Here is a comprehensive list of labels that are provided by the metrics: diff --git a/docs/content/observability/tracing/overview.md b/docs/content/observability/tracing/overview.md index 73504a140..f4126274a 100644 --- a/docs/content/observability/tracing/overview.md +++ b/docs/content/observability/tracing/overview.md @@ -12,7 +12,7 @@ The tracing system allows developers to visualize call flows in their infrastruc Traefik uses OpenTracing, an open standard designed for distributed tracing. -Traefik supports six tracing backends: +Traefik supports seven tracing backends: - [Jaeger](./jaeger.md) - [Zipkin](./zipkin.md) @@ -20,6 +20,7 @@ Traefik supports six tracing backends: - [Instana](./instana.md) - [Haystack](./haystack.md) - [Elastic](./elastic.md) +- [OpenTelemetry](./opentelemetry.md) ## Configuration diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 8fd567356..6dc69cf30 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -95,7 +95,7 @@ and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/). ## Routing Configuration When using Docker as a [provider](./overview.md), -Traefik uses [container labels](https://docs.docker.com/engine/reference/commandline/run/#set-metadata-on-container--l---label---label-file) to retrieve its routing configuration. +Traefik uses [container labels](https://docs.docker.com/engine/reference/commandline/run/#-set-metadata-on-container--l---label---label-file) to retrieve its routing configuration. See the list of labels in the dedicated [routing](../routing/providers/docker.md) section. diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md index 5720889d8..45e01904a 100644 --- a/docs/content/routing/providers/kubernetes-ingress.md +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -888,14 +888,20 @@ TLS certificates can be managed in Secrets objects. ### Communication Between Traefik and Pods +!!! info "It is not possible to route requests directly to [Kubernetes services](https://kubernetes.io/docs/concepts/services-networking/service/ "Link to Kubernetes service docs")" + + You can use an `ExternalName` service to forward requests to the Kubernetes service through DNS. + + For doing so, you have to [allow external name services](https://doc.traefik.io/traefik/providers/kubernetes-ingress/#allowexternalnameservices "Link to docs about allowing external name services"). + Traefik automatically requests endpoint information based on the service provided in the ingress spec. Although Traefik will connect directly to the endpoints (pods), it still checks the service port to see if TLS communication is required. -There are 3 ways to configure Traefik to use https to communicate with pods: +There are 3 ways to configure Traefik to use HTTPS to communicate with pods: 1. If the service port defined in the ingress spec is `443` (note that you can still use `targetPort` to use a different port on your pod). -1. If the service port defined in the ingress spec has a name that starts with https (such as `https-api`, `https-web` or just `https`). +1. If the service port defined in the ingress spec has a name that starts with `https` (such as `https-api`, `https-web` or just `https`). 1. If the service spec includes the annotation `traefik.ingress.kubernetes.io/service.serversscheme: https`. If either of those configuration options exist, then the backend communication protocol is assumed to be TLS, diff --git a/integration/error_pages_test.go b/integration/error_pages_test.go index 436b2cc96..a9bd1c7a1 100644 --- a/integration/error_pages_test.go +++ b/integration/error_pages_test.go @@ -2,6 +2,7 @@ package integration import ( "net/http" + "net/http/httptest" "os" "time" @@ -29,7 +30,7 @@ func (s *ErrorPagesSuite) TestSimpleConfiguration(c *check.C) { file := s.adaptFile(c, "fixtures/error_pages/simple.toml", struct { Server1 string Server2 string - }{s.BackendIP, s.ErrorPageIP}) + }{"http://" + s.BackendIP + ":80", s.ErrorPageIP}) defer os.Remove(file) cmd, display := s.traefikCmd(withConfigFile(file)) @@ -67,3 +68,33 @@ func (s *ErrorPagesSuite) TestErrorPage(c *check.C) { err = try.Request(frontendReq, 2*time.Second, try.BodyContains("An error occurred.")) c.Assert(err, checker.IsNil) } + +func (s *ErrorPagesSuite) TestErrorPageFlush(c *check.C) { + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add("Transfer-Encoding", "chunked") + rw.WriteHeader(http.StatusInternalServerError) + _, _ = rw.Write([]byte("KO")) + })) + + file := s.adaptFile(c, "fixtures/error_pages/simple.toml", struct { + Server1 string + Server2 string + }{srv.URL, s.ErrorPageIP}) + defer os.Remove(file) + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer s.killCmd(cmd) + + frontendReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080", nil) + c.Assert(err, checker.IsNil) + frontendReq.Host = "test.local" + + err = try.Request(frontendReq, 2*time.Second, + try.BodyContains("An error occurred."), + try.HasHeaderValue("Content-Type", "text/html", true), + ) + c.Assert(err, checker.IsNil) +} diff --git a/integration/fixtures/error_pages/simple.toml b/integration/fixtures/error_pages/simple.toml index a60f7393d..9ff846f74 100644 --- a/integration/fixtures/error_pages/simple.toml +++ b/integration/fixtures/error_pages/simple.toml @@ -31,7 +31,7 @@ [http.services.service1.loadBalancer] passHostHeader = true [[http.services.service1.loadBalancer.servers]] - url = "http://{{.Server1}}:80" + url = "{{.Server1}}" [http.services.error.loadBalancer] [[http.services.error.loadBalancer.servers]] diff --git a/integration/testdata/rawdata-crd-label-selector.json b/integration/testdata/rawdata-crd-label-selector.json index 63ace05eb..4e9df8d33 100644 --- a/integration/testdata/rawdata-crd-label-selector.json +++ b/integration/testdata/rawdata-crd-label-selector.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "priority": 18, "status": "enabled", "using": [ "web" diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index 2e5c308bd..5afe2f0a8 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "priority": 18, "status": "enabled", "using": [ "web" @@ -35,6 +36,7 @@ ], "service": "default-test2-route-23c7f4c450289ee29016", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/tobestripped`)", + "priority": 46, "status": "enabled", "using": [ "web" @@ -46,6 +48,7 @@ ], "service": "default-wrr1", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/wrr1`)", + "priority": 38, "status": "enabled", "using": [ "web" @@ -57,6 +60,7 @@ ], "service": "default-testst-route-60ad45fcb5fc1f5f3629", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/serverstransport`)", + "priority": 50, "status": "enabled", "using": [ "web" @@ -68,6 +72,7 @@ ], "service": "other-ns-wrr3", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/c`)", + "priority": 35, "error": [ "the service \"other-ns-wrr3@kubernetescrd\" does not exist" ], @@ -261,6 +266,7 @@ ], "service": "default-test3.route-673acf455cb2dab0b43a", "rule": "HostSNI(`*`)", + "priority": -1, "tls": { "passthrough": false, "options": "default-mytlsoption" diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json index 46a60e58f..9c695ef06 100644 --- a/integration/testdata/rawdata-gateway.json +++ b/integration/testdata/rawdata-gateway.json @@ -34,6 +34,7 @@ ], "service": "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", "rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)", + "priority": 31, "status": "enabled", "using": [ "web" @@ -45,6 +46,7 @@ ], "service": "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06-wrr", "rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)", + "priority": 31, "tls": {}, "status": "enabled", "using": [ @@ -150,6 +152,7 @@ ], "service": "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0", "rule": "HostSNI(`*`)", + "priority": -1, "status": "enabled", "using": [ "footcp" @@ -161,6 +164,7 @@ ], "service": "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0", "rule": "HostSNI(`*`)", + "priority": -1, "tls": { "passthrough": false }, @@ -175,6 +179,7 @@ ], "service": "default-tls-app-1-my-tls-gateway-footlspassthrough-2279fe75c5156dc5eb26-wrr-0", "rule": "HostSNI(`foo.bar`)", + "priority": 18, "tls": { "passthrough": true }, diff --git a/integration/testdata/rawdata-ingress-label-selector.json b/integration/testdata/rawdata-ingress-label-selector.json index 54fbae930..d72826ae7 100644 --- a/integration/testdata/rawdata-ingress-label-selector.json +++ b/integration/testdata/rawdata-ingress-label-selector.json @@ -34,6 +34,7 @@ ], "service": "default-whoami-http", "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", + "priority": 44, "status": "enabled", "using": [ "web" diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index a912d1ea0..ecfa38ee6 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -34,6 +34,7 @@ ], "service": "default-whoami-http", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", + "priority": 50, "status": "enabled", "using": [ "web" @@ -45,6 +46,7 @@ ], "service": "default-whoami-http", "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", + "priority": 44, "status": "enabled", "using": [ "web" @@ -56,6 +58,7 @@ ], "service": "default-whoami-80", "rule": "Host(`whoami.test.drop`) \u0026\u0026 PathPrefix(`/drop`)", + "priority": 47, "status": "enabled", "using": [ "web" @@ -67,6 +70,7 @@ ], "service": "default-whoami-80", "rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)", + "priority": 47, "status": "enabled", "using": [ "web" diff --git a/integration/testdata/rawdata-ingressclass.json b/integration/testdata/rawdata-ingressclass.json index e592135bc..4215504c0 100644 --- a/integration/testdata/rawdata-ingressclass.json +++ b/integration/testdata/rawdata-ingressclass.json @@ -34,6 +34,7 @@ ], "service": "default-whoami-80", "rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)", + "priority": 47, "status": "enabled", "using": [ "web" diff --git a/pkg/api/criterion.go b/pkg/api/criterion.go index d81a9f717..2e3d2f7e6 100644 --- a/pkg/api/criterion.go +++ b/pkg/api/criterion.go @@ -22,8 +22,10 @@ type pageInfo struct { } type searchCriterion struct { - Search string `url:"search"` - Status string `url:"status"` + Search string `url:"search"` + Status string `url:"status"` + ServiceName string `url:"serviceName"` + MiddlewareName string `url:"middlewareName"` } func newSearchCriterion(query url.Values) *searchCriterion { @@ -33,12 +35,19 @@ func newSearchCriterion(query url.Values) *searchCriterion { search := query.Get("search") status := query.Get("status") + serviceName := query.Get("serviceName") + middlewareName := query.Get("middlewareName") - if status == "" && search == "" { + if status == "" && search == "" && serviceName == "" && middlewareName == "" { return nil } - return &searchCriterion{Search: search, Status: status} + return &searchCriterion{ + Search: search, + Status: status, + ServiceName: serviceName, + MiddlewareName: middlewareName, + } } func (c *searchCriterion) withStatus(name string) bool { @@ -59,6 +68,34 @@ func (c *searchCriterion) searchIn(values ...string) bool { return false } +func (c *searchCriterion) filterService(name string) bool { + if c.ServiceName == "" { + return true + } + + if strings.Contains(name, "@") { + return c.ServiceName == name + } + + before, _, _ := strings.Cut(c.ServiceName, "@") + + return before == name +} + +func (c *searchCriterion) filterMiddleware(mns []string) bool { + if c.MiddlewareName == "" { + return true + } + + for _, mn := range mns { + if c.MiddlewareName == mn { + return true + } + } + + return false +} + func pagination(request *http.Request, max int) (pageInfo, error) { perPage, err := getIntParam(request, "per_page", defaultPerPage) if err != nil { diff --git a/pkg/api/handler_http.go b/pkg/api/handler_http.go index ebf35d206..9ee716e2d 100644 --- a/pkg/api/handler_http.go +++ b/pkg/api/handler_http.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "sort" "strconv" "strings" @@ -69,7 +68,8 @@ func newMiddlewareRepresentation(name string, mi *runtime.MiddlewareInfo) middle func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) { results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, rt := range h.runtimeConfiguration.Routers { if keepRouter(name, rt, criterion) { @@ -77,9 +77,7 @@ func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortRouters(query, results) rw.Header().Set("Content-Type", "application/json") @@ -121,7 +119,8 @@ func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) { func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) { results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, si := range h.runtimeConfiguration.Services { if keepService(name, si, criterion) { @@ -129,9 +128,7 @@ func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortServices(query, results) rw.Header().Set("Content-Type", "application/json") @@ -173,7 +170,8 @@ func (h Handler) getService(rw http.ResponseWriter, request *http.Request) { func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) { results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, mi := range h.runtimeConfiguration.Middlewares { if keepMiddleware(name, mi, criterion) { @@ -181,9 +179,7 @@ func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortMiddlewares(query, results) rw.Header().Set("Content-Type", "application/json") @@ -227,7 +223,10 @@ func keepRouter(name string, item *runtime.RouterInfo, criterion *searchCriterio return true } - return criterion.withStatus(item.Status) && criterion.searchIn(item.Rule, name) + return criterion.withStatus(item.Status) && + criterion.searchIn(item.Rule, name) && + criterion.filterService(item.Service) && + criterion.filterMiddleware(item.Middlewares) } func keepService(name string, item *runtime.ServiceInfo, criterion *searchCriterion) bool { diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go index f6137f202..238b59454 100644 --- a/pkg/api/handler_http_test.go +++ b/pkg/api/handler_http_test.go @@ -202,6 +202,84 @@ func TestHandler_HTTP(t *testing.T) { jsonFile: "testdata/routers-filtered-search.json", }, }, + { + desc: "routers filtered by service", + path: "/api/http/routers?serviceName=fii-service@myprovider", + conf: runtime.Configuration{ + Routers: map[string]*runtime.RouterInfo{ + "test@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "fii-service@myprovider", + Rule: "Host(`fii.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + Status: runtime.StatusEnabled, + }, + "foo@otherprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "fii-service", + Rule: "Host(`fii.foo.other`)", + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/routers-filtered-serviceName.json", + }, + }, + { + desc: "routers filtered by middleware", + path: "/api/http/routers?middlewareName=auth", + conf: runtime.Configuration{ + Routers: map[string]*runtime.RouterInfo{ + "test@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "fii-service@myprovider", + Rule: "Host(`fii.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + Status: runtime.StatusEnabled, + }, + "foo@otherprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "fii-service", + Rule: "Host(`fii.foo.other`)", + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/routers-filtered-middlewareName.json", + }, + }, { desc: "one router by id", path: "/api/http/routers/bar@myprovider", diff --git a/pkg/api/handler_tcp.go b/pkg/api/handler_tcp.go index 8efb13068..43aeb9be0 100644 --- a/pkg/api/handler_tcp.go +++ b/pkg/api/handler_tcp.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "sort" "strconv" "strings" @@ -62,7 +61,8 @@ func newTCPMiddlewareRepresentation(name string, mi *runtime.TCPMiddlewareInfo) func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) { results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, rt := range h.runtimeConfiguration.TCPRouters { if keepTCPRouter(name, rt, criterion) { @@ -70,9 +70,7 @@ func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortRouters(query, results) rw.Header().Set("Content-Type", "application/json") @@ -114,7 +112,8 @@ func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) { func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) { results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, si := range h.runtimeConfiguration.TCPServices { if keepTCPService(name, si, criterion) { @@ -122,9 +121,7 @@ func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortServices(query, results) rw.Header().Set("Content-Type", "application/json") @@ -166,7 +163,8 @@ func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) { func (h Handler) getTCPMiddlewares(rw http.ResponseWriter, request *http.Request) { results := make([]tcpMiddlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, mi := range h.runtimeConfiguration.TCPMiddlewares { if keepTCPMiddleware(name, mi, criterion) { @@ -174,9 +172,7 @@ func (h Handler) getTCPMiddlewares(rw http.ResponseWriter, request *http.Request } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortMiddlewares(query, results) rw.Header().Set("Content-Type", "application/json") @@ -220,7 +216,10 @@ func keepTCPRouter(name string, item *runtime.TCPRouterInfo, criterion *searchCr return true } - return criterion.withStatus(item.Status) && criterion.searchIn(item.Rule, name) + return criterion.withStatus(item.Status) && + criterion.searchIn(item.Rule, name) && + criterion.filterService(item.Service) && + criterion.filterMiddleware(item.Middlewares) } func keepTCPService(name string, item *runtime.TCPServiceInfo, criterion *searchCriterion) bool { diff --git a/pkg/api/handler_tcp_test.go b/pkg/api/handler_tcp_test.go index 18ac7708c..d8a6e0867 100644 --- a/pkg/api/handler_tcp_test.go +++ b/pkg/api/handler_tcp_test.go @@ -193,6 +193,89 @@ func TestHandler_TCP(t *testing.T) { jsonFile: "testdata/tcprouters-filtered-search.json", }, }, + { + desc: "TCP routers filtered by service", + path: "/api/tcp/routers?serviceName=foo-service@myprovider", + conf: runtime.Configuration{ + TCPRouters: map[string]*runtime.TCPRouterInfo{ + "test@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Passthrough: false, + }, + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + }, + Status: runtime.StatusWarning, + }, + "foo@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "bar-service@myprovider", + Rule: "Host(`foo.bar`)", + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/tcprouters-filtered-serviceName.json", + }, + }, + { + desc: "TCP routers filtered by middleware", + path: "/api/tcp/routers?middlewareName=auth", + conf: runtime.Configuration{ + TCPRouters: map[string]*runtime.TCPRouterInfo{ + "test@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"inflightconn@myprovider"}, + TLS: &dynamic.RouterTCPTLSConfig{ + Passthrough: false, + }, + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "inflightconn@myprovider"}, + }, + Status: runtime.StatusWarning, + }, + "foo@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "bar-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"inflightconn@myprovider", "auth"}, + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/tcprouters-filtered-middlewareName.json", + }, + }, { desc: "one TCP router by id", path: "/api/tcp/routers/bar@myprovider", diff --git a/pkg/api/handler_udp.go b/pkg/api/handler_udp.go index 13adfafea..72bffc80e 100644 --- a/pkg/api/handler_udp.go +++ b/pkg/api/handler_udp.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "sort" "strconv" "strings" @@ -46,7 +45,8 @@ func newUDPServiceRepresentation(name string, si *runtime.UDPServiceInfo) udpSer func (h Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) { results := make([]udpRouterRepresentation, 0, len(h.runtimeConfiguration.UDPRouters)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, rt := range h.runtimeConfiguration.UDPRouters { if keepUDPRouter(name, rt, criterion) { @@ -54,9 +54,7 @@ func (h Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortRouters(query, results) rw.Header().Set("Content-Type", "application/json") @@ -98,7 +96,8 @@ func (h Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) { func (h Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) { results := make([]udpServiceRepresentation, 0, len(h.runtimeConfiguration.UDPServices)) - criterion := newSearchCriterion(request.URL.Query()) + query := request.URL.Query() + criterion := newSearchCriterion(query) for name, si := range h.runtimeConfiguration.UDPServices { if keepUDPService(name, si, criterion) { @@ -106,9 +105,7 @@ func (h Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) { } } - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) + sortServices(query, results) rw.Header().Set("Content-Type", "application/json") @@ -152,7 +149,9 @@ func keepUDPRouter(name string, item *runtime.UDPRouterInfo, criterion *searchCr return true } - return criterion.withStatus(item.Status) && criterion.searchIn(name) + return criterion.withStatus(item.Status) && + criterion.searchIn(name) && + criterion.filterService(item.Service) } func keepUDPService(name string, item *runtime.UDPServiceInfo, criterion *searchCriterion) bool { diff --git a/pkg/api/handler_udp_test.go b/pkg/api/handler_udp_test.go index 4a5c0116f..2c7842fae 100644 --- a/pkg/api/handler_udp_test.go +++ b/pkg/api/handler_udp_test.go @@ -172,6 +172,40 @@ func TestHandler_UDP(t *testing.T) { jsonFile: "testdata/udprouters-filtered-search.json", }, }, + { + desc: "UDP routers filtered by service", + path: "/api/udp/routers?serviceName=foo-service@myprovider", + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "test@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + Status: runtime.StatusEnabled, + }, + "bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service", + }, + Status: runtime.StatusWarning, + }, + "foo@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "bar-service@myprovider", + }, + Status: runtime.StatusDisabled, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/udprouters-filtered-serviceName.json", + }, + }, { desc: "one UDP router by id", path: "/api/udp/routers/bar@myprovider", diff --git a/pkg/api/sort.go b/pkg/api/sort.go new file mode 100644 index 000000000..58e1cde28 --- /dev/null +++ b/pkg/api/sort.go @@ -0,0 +1,386 @@ +package api + +import ( + "net/url" + "sort" + + "golang.org/x/exp/constraints" +) + +const ( + sortByParam = "sortBy" + directionParam = "direction" +) + +const ( + ascendantSorting = "asc" + descendantSorting = "desc" +) + +type orderedWithName interface { + name() string +} + +type orderedRouter interface { + orderedWithName + + provider() string + priority() int + status() string + rule() string + service() string + entryPointsCount() int +} + +func sortRouters[T orderedRouter](values url.Values, routers []T) { + sortBy := values.Get(sortByParam) + + direction := values.Get(directionParam) + if direction == "" { + direction = ascendantSorting + } + + switch sortBy { + case "name": + sortByName(direction, routers) + + case "provider": + sortByFunc(direction, routers, func(i int) string { return routers[i].provider() }) + + case "priority": + sortByFunc(direction, routers, func(i int) int { return routers[i].priority() }) + + case "status": + sortByFunc(direction, routers, func(i int) string { return routers[i].status() }) + + case "rule": + sortByFunc(direction, routers, func(i int) string { return routers[i].rule() }) + + case "service": + sortByFunc(direction, routers, func(i int) string { return routers[i].service() }) + + case "entryPoints": + sortByFunc(direction, routers, func(i int) int { return routers[i].entryPointsCount() }) + + default: + sortByName(direction, routers) + } +} + +func (r routerRepresentation) name() string { + return r.Name +} + +func (r routerRepresentation) provider() string { + return r.Provider +} + +func (r routerRepresentation) priority() int { + return r.Priority +} + +func (r routerRepresentation) status() string { + return r.Status +} + +func (r routerRepresentation) rule() string { + return r.Rule +} + +func (r routerRepresentation) service() string { + return r.Service +} + +func (r routerRepresentation) entryPointsCount() int { + return len(r.EntryPoints) +} + +func (r tcpRouterRepresentation) name() string { + return r.Name +} + +func (r tcpRouterRepresentation) provider() string { + return r.Provider +} + +func (r tcpRouterRepresentation) priority() int { + return r.Priority +} + +func (r tcpRouterRepresentation) status() string { + return r.Status +} + +func (r tcpRouterRepresentation) rule() string { + return r.Rule +} + +func (r tcpRouterRepresentation) service() string { + return r.Service +} + +func (r tcpRouterRepresentation) entryPointsCount() int { + return len(r.EntryPoints) +} + +func (r udpRouterRepresentation) name() string { + return r.Name +} + +func (r udpRouterRepresentation) provider() string { + return r.Provider +} + +func (r udpRouterRepresentation) priority() int { + // noop + return 0 +} + +func (r udpRouterRepresentation) status() string { + return r.Status +} + +func (r udpRouterRepresentation) rule() string { + // noop + return "" +} + +func (r udpRouterRepresentation) service() string { + return r.Service +} + +func (r udpRouterRepresentation) entryPointsCount() int { + return len(r.EntryPoints) +} + +type orderedService interface { + orderedWithName + + resourceType() string + serversCount() int + provider() string + status() string +} + +func sortServices[T orderedService](values url.Values, services []T) { + sortBy := values.Get(sortByParam) + + direction := values.Get(directionParam) + if direction == "" { + direction = ascendantSorting + } + + switch sortBy { + case "name": + sortByName(direction, services) + + case "type": + sortByFunc(direction, services, func(i int) string { return services[i].resourceType() }) + + case "servers": + sortByFunc(direction, services, func(i int) int { return services[i].serversCount() }) + + case "provider": + sortByFunc(direction, services, func(i int) string { return services[i].provider() }) + + case "status": + sortByFunc(direction, services, func(i int) string { return services[i].status() }) + + default: + sortByName(direction, services) + } +} + +func (s serviceRepresentation) name() string { + return s.Name +} + +func (s serviceRepresentation) resourceType() string { + return s.Type +} + +func (s serviceRepresentation) serversCount() int { + // TODO: maybe disable that data point altogether, + // if we can't/won't compute a fully correct (recursive) result. + // Or "redefine" it as only the top-level count? + // Note: The current algo is equivalent to the webui one. + if s.LoadBalancer == nil { + return 0 + } + + return len(s.LoadBalancer.Servers) +} + +func (s serviceRepresentation) provider() string { + return s.Provider +} + +func (s serviceRepresentation) status() string { + return s.Status +} + +func (s tcpServiceRepresentation) name() string { + return s.Name +} + +func (s tcpServiceRepresentation) resourceType() string { + return s.Type +} + +func (s tcpServiceRepresentation) serversCount() int { + // TODO: maybe disable that data point altogether, + // if we can't/won't compute a fully correct (recursive) result. + // Or "redefine" it as only the top-level count? + // Note: The current algo is equivalent to the webui one. + if s.LoadBalancer == nil { + return 0 + } + + return len(s.LoadBalancer.Servers) +} + +func (s tcpServiceRepresentation) provider() string { + return s.Provider +} + +func (s tcpServiceRepresentation) status() string { + return s.Status +} + +func (s udpServiceRepresentation) name() string { + return s.Name +} + +func (s udpServiceRepresentation) resourceType() string { + return s.Type +} + +func (s udpServiceRepresentation) serversCount() int { + // TODO: maybe disable that data point altogether, + // if we can't/won't compute a fully correct (recursive) result. + // Or "redefine" it as only the top-level count? + // Note: The current algo is equivalent to the webui one. + if s.LoadBalancer == nil { + return 0 + } + + return len(s.LoadBalancer.Servers) +} + +func (s udpServiceRepresentation) provider() string { + return s.Provider +} + +func (s udpServiceRepresentation) status() string { + return s.Status +} + +type orderedMiddleware interface { + orderedWithName + + resourceType() string + provider() string + status() string +} + +func sortMiddlewares[T orderedMiddleware](values url.Values, middlewares []T) { + sortBy := values.Get(sortByParam) + + direction := values.Get(directionParam) + if direction == "" { + direction = ascendantSorting + } + + switch sortBy { + case "name": + sortByName(direction, middlewares) + + case "type": + sortByFunc(direction, middlewares, func(i int) string { return middlewares[i].resourceType() }) + + case "provider": + sortByFunc(direction, middlewares, func(i int) string { return middlewares[i].provider() }) + + case "status": + sortByFunc(direction, middlewares, func(i int) string { return middlewares[i].status() }) + + default: + sortByName(direction, middlewares) + } +} + +func (m middlewareRepresentation) name() string { + return m.Name +} + +func (m middlewareRepresentation) resourceType() string { + return m.Type +} + +func (m middlewareRepresentation) provider() string { + return m.Provider +} + +func (m middlewareRepresentation) status() string { + return m.Status +} + +func (m tcpMiddlewareRepresentation) name() string { + return m.Name +} + +func (m tcpMiddlewareRepresentation) resourceType() string { + return m.Type +} + +func (m tcpMiddlewareRepresentation) provider() string { + return m.Provider +} + +func (m tcpMiddlewareRepresentation) status() string { + return m.Status +} + +type orderedByName interface { + orderedWithName +} + +func sortByName[T orderedByName](direction string, results []T) { + // Ascending + if direction == ascendantSorting { + sort.Slice(results, func(i, j int) bool { + return results[i].name() < results[j].name() + }) + + return + } + + // Descending + sort.Slice(results, func(i, j int) bool { + return results[i].name() > results[j].name() + }) +} + +func sortByFunc[T orderedWithName, U constraints.Ordered](direction string, results []T, fn func(int) U) { + // Ascending + if direction == ascendantSorting { + sort.Slice(results, func(i, j int) bool { + if fn(i) == fn(j) { + return results[i].name() < results[j].name() + } + + return fn(i) < fn(j) + }) + + return + } + + // Descending + sort.Slice(results, func(i, j int) bool { + if fn(i) == fn(j) { + return results[i].name() > results[j].name() + } + + return fn(i) > fn(j) + }) +} diff --git a/pkg/api/sort_test.go b/pkg/api/sort_test.go new file mode 100644 index 000000000..5acab875b --- /dev/null +++ b/pkg/api/sort_test.go @@ -0,0 +1,1689 @@ +package api + +import ( + "fmt" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v2/pkg/config/dynamic" + "github.com/traefik/traefik/v2/pkg/config/runtime" +) + +func TestSortRouters(t *testing.T) { + testCases := []struct { + direction string + sortBy string + elements []orderedRouter + expected []orderedRouter + }{ + { + direction: ascendantSorting, + sortBy: "name", + elements: []orderedRouter{ + routerRepresentation{ + Name: "b", + }, + routerRepresentation{ + Name: "a", + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + }, + routerRepresentation{ + Name: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "name", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + }, + routerRepresentation{ + Name: "b", + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + }, + routerRepresentation{ + Name: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "provider", + elements: []orderedRouter{ + routerRepresentation{ + Name: "b", + Provider: "b", + }, + routerRepresentation{ + Name: "b", + Provider: "a", + }, + routerRepresentation{ + Name: "a", + Provider: "b", + }, + routerRepresentation{ + Name: "a", + Provider: "a", + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + Provider: "a", + }, + routerRepresentation{ + Name: "b", + Provider: "a", + }, + routerRepresentation{ + Name: "a", + Provider: "b", + }, + routerRepresentation{ + Name: "b", + Provider: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "provider", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + Provider: "a", + }, + routerRepresentation{ + Name: "a", + Provider: "b", + }, + routerRepresentation{ + Name: "b", + Provider: "a", + }, + routerRepresentation{ + Name: "b", + Provider: "b", + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + Provider: "b", + }, + routerRepresentation{ + Name: "a", + Provider: "b", + }, + routerRepresentation{ + Name: "b", + Provider: "a", + }, + routerRepresentation{ + Name: "a", + Provider: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "priority", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "priority", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 2, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Priority: 1, + }, + }, + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "status", + elements: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "status", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "b", + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Status: "a", + }, + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "rule", + elements: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "rule", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Rule: "a", + }, + }, + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "service", + elements: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "service", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "b", + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + Service: "a", + }, + }, + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "entryPoints", + elements: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "entryPoints", + elements: []orderedRouter{ + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + }, + expected: []orderedRouter{ + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a", "b"}, + }, + }, + }, + routerRepresentation{ + Name: "b", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + routerRepresentation{ + Name: "a", + RouterInfo: &runtime.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"a"}, + }, + }, + }, + }, + }, + } + for _, test := range testCases { + test := test + t.Run(fmt.Sprintf("%s-%s", test.direction, test.sortBy), func(t *testing.T) { + t.Parallel() + + u, err := url.Parse(fmt.Sprintf("/?direction=%s&sortBy=%s", test.direction, test.sortBy)) + require.NoError(t, err) + + sortRouters(u.Query(), test.elements) + + assert.Equal(t, test.expected, test.elements) + }) + } +} + +func TestSortServices(t *testing.T) { + testCases := []struct { + direction string + sortBy string + elements []orderedService + expected []orderedService + }{ + { + direction: ascendantSorting, + sortBy: "name", + elements: []orderedService{ + serviceRepresentation{ + Name: "b", + }, + serviceRepresentation{ + Name: "a", + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "a", + }, + serviceRepresentation{ + Name: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "name", + elements: []orderedService{ + serviceRepresentation{ + Name: "a", + }, + serviceRepresentation{ + Name: "b", + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "b", + }, + serviceRepresentation{ + Name: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "type", + elements: []orderedService{ + serviceRepresentation{ + Name: "b", + Type: "b", + }, + serviceRepresentation{ + Name: "a", + Type: "b", + }, + serviceRepresentation{ + Name: "b", + Type: "a", + }, + serviceRepresentation{ + Name: "a", + Type: "a", + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "a", + Type: "a", + }, + serviceRepresentation{ + Name: "b", + Type: "a", + }, + serviceRepresentation{ + Name: "a", + Type: "b", + }, + serviceRepresentation{ + Name: "b", + Type: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "type", + elements: []orderedService{ + serviceRepresentation{ + Name: "a", + Type: "a", + }, + serviceRepresentation{ + Name: "b", + Type: "a", + }, + serviceRepresentation{ + Name: "a", + Type: "b", + }, + serviceRepresentation{ + Name: "b", + Type: "b", + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "b", + Type: "b", + }, + serviceRepresentation{ + Name: "a", + Type: "b", + }, + serviceRepresentation{ + Name: "b", + Type: "a", + }, + serviceRepresentation{ + Name: "a", + Type: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "servers", + elements: []orderedService{ + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "servers", + elements: []orderedService{ + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 2), + }, + }, + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: make([]dynamic.Server, 1), + }, + }, + }, + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "provider", + elements: []orderedService{ + serviceRepresentation{ + Name: "b", + Provider: "b", + }, + serviceRepresentation{ + Name: "b", + Provider: "a", + }, + serviceRepresentation{ + Name: "a", + Provider: "b", + }, + serviceRepresentation{ + Name: "a", + Provider: "a", + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "a", + Provider: "a", + }, + serviceRepresentation{ + Name: "b", + Provider: "a", + }, + serviceRepresentation{ + Name: "a", + Provider: "b", + }, + serviceRepresentation{ + Name: "b", + Provider: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "provider", + elements: []orderedService{ + serviceRepresentation{ + Name: "a", + Provider: "a", + }, + serviceRepresentation{ + Name: "a", + Provider: "b", + }, + serviceRepresentation{ + Name: "b", + Provider: "a", + }, + serviceRepresentation{ + Name: "b", + Provider: "b", + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "b", + Provider: "b", + }, + serviceRepresentation{ + Name: "a", + Provider: "b", + }, + serviceRepresentation{ + Name: "b", + Provider: "a", + }, + serviceRepresentation{ + Name: "a", + Provider: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "status", + elements: []orderedService{ + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "status", + elements: []orderedService{ + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + }, + expected: []orderedService{ + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "b", + }, + }, + serviceRepresentation{ + Name: "b", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + serviceRepresentation{ + Name: "a", + ServiceInfo: &runtime.ServiceInfo{ + Status: "a", + }, + }, + }, + }, + } + for _, test := range testCases { + test := test + t.Run(fmt.Sprintf("%s-%s", test.direction, test.sortBy), func(t *testing.T) { + t.Parallel() + + u, err := url.Parse(fmt.Sprintf("/?direction=%s&sortBy=%s", test.direction, test.sortBy)) + require.NoError(t, err) + + sortServices(u.Query(), test.elements) + + assert.Equal(t, test.expected, test.elements) + }) + } +} + +func TestSortMiddlewares(t *testing.T) { + testCases := []struct { + direction string + sortBy string + elements []orderedMiddleware + expected []orderedMiddleware + }{ + { + direction: ascendantSorting, + sortBy: "name", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + }, + middlewareRepresentation{ + Name: "a", + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + }, + middlewareRepresentation{ + Name: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "name", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + }, + middlewareRepresentation{ + Name: "b", + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + }, + middlewareRepresentation{ + Name: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "type", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + Type: "b", + }, + middlewareRepresentation{ + Name: "a", + Type: "b", + }, + middlewareRepresentation{ + Name: "b", + Type: "a", + }, + middlewareRepresentation{ + Name: "a", + Type: "a", + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + Type: "a", + }, + middlewareRepresentation{ + Name: "b", + Type: "a", + }, + middlewareRepresentation{ + Name: "a", + Type: "b", + }, + middlewareRepresentation{ + Name: "b", + Type: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "type", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + Type: "a", + }, + middlewareRepresentation{ + Name: "b", + Type: "a", + }, + middlewareRepresentation{ + Name: "a", + Type: "b", + }, + middlewareRepresentation{ + Name: "b", + Type: "b", + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + Type: "b", + }, + middlewareRepresentation{ + Name: "a", + Type: "b", + }, + middlewareRepresentation{ + Name: "b", + Type: "a", + }, + middlewareRepresentation{ + Name: "a", + Type: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "provider", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + Provider: "b", + }, + middlewareRepresentation{ + Name: "b", + Provider: "a", + }, + middlewareRepresentation{ + Name: "a", + Provider: "b", + }, + middlewareRepresentation{ + Name: "a", + Provider: "a", + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + Provider: "a", + }, + middlewareRepresentation{ + Name: "b", + Provider: "a", + }, + middlewareRepresentation{ + Name: "a", + Provider: "b", + }, + middlewareRepresentation{ + Name: "b", + Provider: "b", + }, + }, + }, + { + direction: descendantSorting, + sortBy: "provider", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + Provider: "a", + }, + middlewareRepresentation{ + Name: "a", + Provider: "b", + }, + middlewareRepresentation{ + Name: "b", + Provider: "a", + }, + middlewareRepresentation{ + Name: "b", + Provider: "b", + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + Provider: "b", + }, + middlewareRepresentation{ + Name: "a", + Provider: "b", + }, + middlewareRepresentation{ + Name: "b", + Provider: "a", + }, + middlewareRepresentation{ + Name: "a", + Provider: "a", + }, + }, + }, + { + direction: ascendantSorting, + sortBy: "status", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + }, + }, + { + direction: descendantSorting, + sortBy: "status", + elements: []orderedMiddleware{ + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + }, + expected: []orderedMiddleware{ + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "b", + }, + }, + middlewareRepresentation{ + Name: "b", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + middlewareRepresentation{ + Name: "a", + MiddlewareInfo: &runtime.MiddlewareInfo{ + Status: "a", + }, + }, + }, + }, + } + for _, test := range testCases { + test := test + t.Run(fmt.Sprintf("%s-%s", test.direction, test.sortBy), func(t *testing.T) { + t.Parallel() + + u, err := url.Parse(fmt.Sprintf("/?direction=%s&sortBy=%s", test.direction, test.sortBy)) + require.NoError(t, err) + + sortMiddlewares(u.Query(), test.elements) + + assert.Equal(t, test.expected, test.elements) + }) + } +} diff --git a/pkg/api/testdata/routers-filtered-middlewareName.json b/pkg/api/testdata/routers-filtered-middlewareName.json new file mode 100644 index 000000000..d227748c5 --- /dev/null +++ b/pkg/api/testdata/routers-filtered-middlewareName.json @@ -0,0 +1,36 @@ +[ + { + "entryPoints": [ + "web" + ], + "middlewares": [ + "auth", + "addPrefixTest@anotherprovider" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "foo-service@myprovider", + "status": "disabled", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "middlewares": [ + "addPrefixTest", + "auth" + ], + "name": "test@myprovider", + "provider": "myprovider", + "rule": "Host(`fii.bar.other`)", + "service": "fii-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/routers-filtered-serviceName.json b/pkg/api/testdata/routers-filtered-serviceName.json new file mode 100644 index 000000000..7d50bc03a --- /dev/null +++ b/pkg/api/testdata/routers-filtered-serviceName.json @@ -0,0 +1,32 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "foo@otherprovider", + "provider": "otherprovider", + "rule": "Host(`fii.foo.other`)", + "service": "fii-service", + "status": "enabled", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "middlewares": [ + "addPrefixTest", + "auth" + ], + "name": "test@myprovider", + "provider": "myprovider", + "rule": "Host(`fii.bar.other`)", + "service": "fii-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/tcprouters-filtered-middlewareName.json b/pkg/api/testdata/tcprouters-filtered-middlewareName.json new file mode 100644 index 000000000..e3f5352ec --- /dev/null +++ b/pkg/api/testdata/tcprouters-filtered-middlewareName.json @@ -0,0 +1,36 @@ +[ + { + "entryPoints": [ + "web" + ], + "middlewares": [ + "auth", + "inflightconn@myprovider" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "foo-service", + "status": "warning", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "middlewares": [ + "inflightconn@myprovider", + "auth" + ], + "name": "foo@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "bar-service@myprovider", + "status": "disabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/tcprouters-filtered-serviceName.json b/pkg/api/testdata/tcprouters-filtered-serviceName.json new file mode 100644 index 000000000..6bf20f9db --- /dev/null +++ b/pkg/api/testdata/tcprouters-filtered-serviceName.json @@ -0,0 +1,31 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "foo-service", + "status": "warning", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "name": "test@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar.other`)", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ], + "tls": { + "passthrough": false + } + } +] \ No newline at end of file diff --git a/pkg/api/testdata/udprouters-filtered-serviceName.json b/pkg/api/testdata/udprouters-filtered-serviceName.json new file mode 100644 index 000000000..bf387bfbc --- /dev/null +++ b/pkg/api/testdata/udprouters-filtered-serviceName.json @@ -0,0 +1,26 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "service": "foo-service", + "status": "warning", + "using": [ + "web" + ] + }, + { + "entryPoints": [ + "web" + ], + "name": "test@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index 55684f82e..eda8c4811 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -209,6 +209,15 @@ func (cc *codeCatcher) Flush() { // Otherwise, cc.code is actually a 200 here. cc.WriteHeader(cc.code) + // We don't care about the contents of the response, + // since we want to serve the ones from the error page, + // so we just don't flush. + // (e.g., To prevent superfluous WriteHeader on request with a + // `Transfert-Encoding: chunked` header). + if cc.caughtFilteredCode { + return + } + if flusher, ok := cc.responseWriter.(http.Flusher); ok { flusher.Flush() } diff --git a/pkg/middlewares/ratelimiter/rate_limiter.go b/pkg/middlewares/ratelimiter/rate_limiter.go index 303b20e8e..9b95ab176 100644 --- a/pkg/middlewares/ratelimiter/rate_limiter.go +++ b/pkg/middlewares/ratelimiter/rate_limiter.go @@ -81,10 +81,12 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name period = time.Second } - // if config.Average == 0, in that case, - // the value of maxDelay does not matter since the reservation will (buggily) give us a delay of 0 anyway. + // Initialized at rate.Inf to enforce no rate limiting when config.Average == 0 + rtl := float64(rate.Inf) + // No need to set any particular value for maxDelay as the reservation's delay + // will be <= 0 in the Inf case (i.e. the average == 0 case). var maxDelay time.Duration - var rtl float64 + if config.Average > 0 { rtl = float64(config.Average*int64(time.Second)) / float64(period) // maxDelay does not scale well for rates below 1, @@ -155,10 +157,6 @@ func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - // time/rate is bugged, since a rate.Limiter with a 0 Limit not only allows a Reservation to take place, - // but also gives a 0 delay below (because of a division by zero, followed by a multiplication that flips into the negatives), - // regardless of the current load. - // However, for now we take advantage of this behavior to provide the no-limit ratelimiter when config.Average is 0. res := bucket.Reserve() if !res.OK() { http.Error(rw, "No bursty traffic allowed", http.StatusTooManyRequests) diff --git a/pkg/middlewares/ratelimiter/rate_limiter_test.go b/pkg/middlewares/ratelimiter/rate_limiter_test.go index ac73fc2ec..670561774 100644 --- a/pkg/middlewares/ratelimiter/rate_limiter_test.go +++ b/pkg/middlewares/ratelimiter/rate_limiter_test.go @@ -15,6 +15,7 @@ import ( "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/testhelpers" "github.com/vulcand/oxy/v2/utils" + "golang.org/x/time/rate" ) func TestNewRateLimiter(t *testing.T) { @@ -25,7 +26,16 @@ func TestNewRateLimiter(t *testing.T) { expectedSourceIP string requestHeader string expectedError string + expectedRTL rate.Limit }{ + { + desc: "no ratelimit on Average == 0", + config: dynamic.RateLimit{ + Average: 0, + Burst: 10, + }, + expectedRTL: rate.Inf, + }, { desc: "maxDelay computation", config: dynamic.RateLimit{ @@ -120,6 +130,9 @@ func TestNewRateLimiter(t *testing.T) { assert.NoError(t, err) assert.Equal(t, test.requestHeader, hd) } + if test.expectedRTL != 0 { + assert.Equal(t, test.expectedRTL, rtl.rate) + } }) } } diff --git a/pkg/muxer/http/mux.go b/pkg/muxer/http/mux.go index 3039075ee..6111af74c 100644 --- a/pkg/muxer/http/mux.go +++ b/pkg/muxer/http/mux.go @@ -46,6 +46,12 @@ func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { http.NotFoundHandler().ServeHTTP(rw, req) } +// GetRulePriority computes the priority for a given rule. +// The priority is calculated using the length of rule. +func GetRulePriority(rule string) int { + return len(rule) +} + // AddRoute add a new route to the router. func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error { parse, err := m.parser.Parse(rule) @@ -64,10 +70,6 @@ func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error return fmt.Errorf("error while adding rule %s: %w", rule, err) } - if priority == 0 { - priority = len(rule) - } - m.routes = append(m.routes, &route{ handler: handler, matchers: matchers, diff --git a/pkg/muxer/http/mux_test.go b/pkg/muxer/http/mux_test.go index 0c1903de9..e11502b1b 100644 --- a/pkg/muxer/http/mux_test.go +++ b/pkg/muxer/http/mux_test.go @@ -376,6 +376,10 @@ func Test_addRoutePriority(t *testing.T) { w.Header().Set("X-From", route.xFrom) }) + if route.priority == 0 { + route.priority = GetRulePriority(route.rule) + } + err := muxer.AddRoute(route.rule, route.priority, handler) require.NoError(t, err, route.rule) } @@ -517,3 +521,26 @@ func TestEmptyHost(t *testing.T) { }) } } + +func TestGetRulePriority(t *testing.T) { + testCases := []struct { + desc string + rule string + expected int + }{ + { + desc: "simple rule", + rule: "Host(`example.org`)", + expected: 19, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, GetRulePriority(test.rule)) + }) + } +} diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go index 00ec577af..0c6ef3fd6 100644 --- a/pkg/muxer/tcp/mux.go +++ b/pkg/muxer/tcp/mux.go @@ -72,6 +72,38 @@ func (m Muxer) Match(meta ConnData) (tcp.Handler, bool) { return nil, false } +// GetRulePriority computes the priority for a given rule. +// The priority is calculated using the length of rule. +// There is a special case where the HostSNI(`*`) has a priority of -1. +func GetRulePriority(rule string) int { + catchAllParser, err := rules.NewParser([]string{"HostSNI"}) + if err != nil { + return len(rule) + } + + parse, err := catchAllParser.Parse(rule) + if err != nil { + return len(rule) + } + + buildTree, ok := parse.(rules.TreeBuilder) + if !ok { + return len(rule) + } + + ruleTree := buildTree() + + // Special case for when the catchAll fallback is present. + // When no user-defined priority is found, the lowest computable priority minus one is used, + // in order to make the fallback the last to be evaluated. + if ruleTree.RuleLeft == nil && ruleTree.RuleRight == nil && len(ruleTree.Value) == 1 && + ruleTree.Value[0] == "*" && strings.EqualFold(ruleTree.Matcher, "HostSNI") { + return -1 + } + + return len(rule) +} + // AddRoute adds a new route, associated to the given handler, at the given // priority, to the muxer. func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error { @@ -98,18 +130,6 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error { catchAll = ruleTree.Value[0] == "*" && strings.EqualFold(ruleTree.Matcher, "HostSNI") } - // Special case for when the catchAll fallback is present. - // When no user-defined priority is found, the lowest computable priority minus one is used, - // in order to make the fallback the last to be evaluated. - if priority == 0 && catchAll { - priority = -1 - } - - // Default value, which means the user has not set it, so we'll compute it. - if priority == 0 { - priority = len(rule) - } - newRoute := &route{ handler: handler, matchers: matchers, diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go index 95e84a245..54cd976cf 100644 --- a/pkg/muxer/tcp/mux_test.go +++ b/pkg/muxer/tcp/mux_test.go @@ -444,6 +444,39 @@ func Test_Priority(t *testing.T) { } } +func TestGetRulePriority(t *testing.T) { + testCases := []struct { + desc string + rule string + expected int + }{ + { + desc: "simple rule", + rule: "HostSNI(`example.org`)", + expected: 22, + }, + { + desc: "HostSNI(`*`) rule", + rule: "HostSNI(`*`)", + expected: -1, + }, + { + desc: "strange HostSNI(`*`) rule", + rule: " HostSNI ( `*` ) ", + expected: -1, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, GetRulePriority(test.rule)) + }) + } +} + type fakeConn struct { call map[string]int remoteAddr net.Addr diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index f19a46eb5..067dc9f20 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -119,6 +119,10 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string logger := log.Ctx(ctx).With().Str(logs.RouterName, routerName).Logger() ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerName)) + if routerConfig.Priority == 0 { + routerConfig.Priority = httpmuxer.GetRulePriority(routerConfig.Rule) + } + handler, err := m.buildRouterHandler(ctxRouter, routerName, routerConfig) if err != nil { routerConfig.AddError(err, true) @@ -126,8 +130,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string continue } - err = muxer.AddRoute(routerConfig.Rule, routerConfig.Priority, handler) - if err != nil { + if err = muxer.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() continue diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index 9bbadbc20..05a451131 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -264,6 +264,10 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim logger := log.Ctx(ctx).With().Str(logs.RouterName, routerName).Logger() ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerName)) + if routerConfig.Priority == 0 { + routerConfig.Priority = tcpmuxer.GetRulePriority(routerConfig.Rule) + } + if routerConfig.Service == "" { err := errors.New("the service is missing on the router") routerConfig.AddError(err, true) @@ -306,6 +310,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim if routerConfig.TLS == nil { logger.Debug().Msgf("Adding route for %q", routerConfig.Rule) + if err := router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() @@ -315,6 +320,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim if routerConfig.TLS.Passthrough { logger.Debug().Msgf("Adding Passthrough route for %q", routerConfig.Rule) + if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() @@ -349,11 +355,11 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim logger.Debug().Msgf("Adding special TLS closing route for %q because broken TLS options %s", routerConfig.Rule, tlsOptionsName) - err = router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, &brokenTLSRouter{}) - if err != nil { + if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, &brokenTLSRouter{}); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() } + continue } @@ -383,10 +389,10 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule) - err = router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler) - if err != nil { + if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() + continue } } } diff --git a/pkg/server/router/tcp/postgres.go b/pkg/server/router/tcp/postgres.go index b79c3762f..02cb480a4 100644 --- a/pkg/server/router/tcp/postgres.go +++ b/pkg/server/router/tcp/postgres.go @@ -4,6 +4,8 @@ import ( "bufio" "bytes" "errors" + "io" + "net" "sync" "github.com/rs/zerolog/log" @@ -25,7 +27,10 @@ func isPostgres(br *bufio.Reader) (bool, error) { for i := 1; i < len(PostgresStartTLSMsg)+1; i++ { peeked, err := br.Peek(i) if err != nil { - log.Error().Err(err).Msg("Error while Peeking first bytes") + var opErr *net.OpError + if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || opErr.Timeout()) { + log.Error().Err(err).Msg("Error while Peeking first byte") + } return false, err } diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index b3f34ee0d..2f0f11af8 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -268,8 +268,7 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) { // muxerHTTPS only contains single HostSNI rules (and no other kind of rules), // so there's no need for specifying a priority for them. - err := r.muxerHTTPS.AddRoute("HostSNI(`"+sniHost+"`)", 0, tcpHandler) - if err != nil { + if err := r.muxerHTTPS.AddRoute("HostSNI(`"+sniHost+"`)", 0, tcpHandler); err != nil { log.Error().Err(err).Msg("Error while adding route for host") } } diff --git a/webui/src/_mixins/GetTableProps.js b/webui/src/_mixins/GetTableProps.js index 014a397bb..5ca70949d 100644 --- a/webui/src/_mixins/GetTableProps.js +++ b/webui/src/_mixins/GetTableProps.js @@ -11,6 +11,7 @@ const allColumns = [ required: true, label: 'Status', align: 'left', + sortable: true, fieldToProps: row => ({ state: row.status === 'enabled' ? 'positive' : 'negative' }), @@ -20,6 +21,7 @@ const allColumns = [ name: 'tls', align: 'left', label: 'TLS', + sortable: false, fieldToProps: row => ({ isTLS: row.tls }), component: TLSState }, @@ -27,6 +29,7 @@ const allColumns = [ name: 'rule', align: 'left', label: 'Rule', + sortable: true, component: QChip, fieldToProps: () => ({ class: 'app-chip app-chip-rule', dense: true }), content: row => row.rule @@ -35,6 +38,7 @@ const allColumns = [ name: 'entryPoints', align: 'left', label: 'Entrypoints', + sortable: true, component: Chips, fieldToProps: row => ({ classNames: 'app-chip app-chip-entry-points', @@ -46,6 +50,7 @@ const allColumns = [ name: 'name', align: 'left', label: 'Name', + sortable: true, component: QChip, fieldToProps: () => ({ class: 'app-chip app-chip-name', dense: true }), content: row => row.name @@ -54,6 +59,7 @@ const allColumns = [ name: 'type', align: 'left', label: 'Type', + sortable: true, component: QChip, fieldToProps: () => ({ class: 'app-chip app-chip-entry-points', @@ -65,6 +71,7 @@ const allColumns = [ name: 'servers', align: 'right', label: 'Servers', + sortable: true, fieldToProps: () => ({ class: 'servers-label' }), content: function (value) { if (value.loadBalancer && value.loadBalancer.servers) { @@ -78,6 +85,7 @@ const allColumns = [ align: 'left', label: 'Service', component: QChip, + sortable: true, fieldToProps: () => ({ class: 'app-chip app-chip-service', dense: true }), content: row => row.service }, @@ -85,8 +93,23 @@ const allColumns = [ name: 'provider', align: 'center', label: 'Provider', + sortable: true, fieldToProps: row => ({ name: row.provider }), component: ProviderIcon + }, + { + name: 'priority', + align: 'left', + label: 'Priority', + sortable: true, + component: QChip, + fieldToProps: () => ({ class: 'app-chip app-chip-accent', dense: true }), + content: row => { + return { + short: String(row.priority).length > 10 ? String(row.priority).substring(0, 10) + '...' : row.priority, + long: row.priority + } + } } ] @@ -98,7 +121,8 @@ const columnsByResource = { 'name', 'service', 'tls', - 'provider' + 'provider', + 'priority' ], udpRouters: ['status', 'entryPoints', 'name', 'service', 'provider'], services: ['status', 'name', 'type', 'servers', 'provider'], diff --git a/webui/src/_services/HttpService.js b/webui/src/_services/HttpService.js index 0e206b24d..9a1228285 100644 --- a/webui/src/_services/HttpService.js +++ b/webui/src/_services/HttpService.js @@ -4,7 +4,7 @@ import { getTotal } from './utils' const apiBase = '/http' function getAllRouters (params) { - return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}&serviceName=${params.serviceName}&middlewareName=${params.middlewareName}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) @@ -22,7 +22,7 @@ function getRouterByName (name) { } function getAllServices (params) { - return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) @@ -40,7 +40,7 @@ function getServiceByName (name) { } function getAllMiddlewares (params) { - return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) diff --git a/webui/src/_services/TcpService.js b/webui/src/_services/TcpService.js index e5645e9b1..ca33b0120 100644 --- a/webui/src/_services/TcpService.js +++ b/webui/src/_services/TcpService.js @@ -4,7 +4,7 @@ import { getTotal } from './utils' const apiBase = '/tcp' function getAllRouters (params) { - return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}&serviceName=${params.serviceName}&middlewareName=${params.middlewareName}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) @@ -22,7 +22,7 @@ function getRouterByName (name) { } function getAllServices (params) { - return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) @@ -40,7 +40,7 @@ function getServiceByName (name) { } function getAllMiddlewares (params) { - return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/middlewares?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) diff --git a/webui/src/_services/UdpService.js b/webui/src/_services/UdpService.js index fd641d357..549a99c1b 100644 --- a/webui/src/_services/UdpService.js +++ b/webui/src/_services/UdpService.js @@ -4,7 +4,7 @@ import { getTotal } from './utils' const apiBase = '/udp' function getAllRouters (params) { - return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/routers?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}&serviceName=${params.serviceName}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) @@ -22,7 +22,7 @@ function getRouterByName (name) { } function getAllServices (params) { - return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}`) + return APP.api.get(`${apiBase}/services?search=${params.query}&status=${params.status}&per_page=${params.limit}&page=${params.page}&sortBy=${params.sortBy}&direction=${params.direction}`) .then(response => { const { data = [], headers } = response const total = getTotal(headers, params) diff --git a/webui/src/components/_commons/MainTable.vue b/webui/src/components/_commons/MainTable.vue index 85ae32e55..6119b33da 100644 --- a/webui/src/components/_commons/MainTable.vue +++ b/webui/src/components/_commons/MainTable.vue @@ -6,9 +6,12 @@ + v-bind:class="getColumn(column.name).sortable ? `text-${column.align} cursor-pointer`: `text-${column.align}`" + v-bind:key="column.name" + @click="getColumn(column.name).sortable ? onSortClick(column.name) : null"> {{ column.label }} + {{currentSortDir === 'asc' ? 'arrow_drop_down' : 'arrow_drop_up'}} + {{currentSortDir === 'asc' ? 'arrow_drop_down' : 'arrow_drop_up'}} @@ -27,9 +30,19 @@ v-bind:is="getColumn(column.name).component" v-bind="getColumn(column.name).fieldToProps(row)" > -