Merge branch 'v3.0' of github.com:traefik/traefik

This commit is contained in:
baalajimaestro 2023-01-17 13:40:33 +05:30
commit e9fcdb9702
Signed by: baalajimaestro
GPG key ID: F93C394FE9BBAFD5
56 changed files with 3034 additions and 148 deletions

View file

@ -1545,7 +1545,8 @@
"hide": 0,
"includeAll": false,
"multi": false,
"name": "datasource",
"name": "DS_PROMETHEUS",
"label": "datasource",
"options": [],
"query": "prometheus",
"refresh": 1,

View file

@ -1537,7 +1537,8 @@
"hide": 0,
"includeAll": false,
"multi": false,
"name": "datasource",
"name": "DS_PROMETHEUS",
"label": "datasource",
"options": [],
"query": "prometheus",
"refresh": 1,

View file

@ -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.

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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,

View file

@ -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)
}

View file

@ -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]]

View file

@ -6,6 +6,7 @@
],
"service": "api@internal",
"rule": "PathPrefix(`/api`)",
"priority": 18,
"status": "enabled",
"using": [
"web"

View file

@ -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"

View file

@ -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
},

View file

@ -34,6 +34,7 @@
],
"service": "default-whoami-http",
"rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)",
"priority": 44,
"status": "enabled",
"using": [
"web"

View file

@ -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"

View file

@ -34,6 +34,7 @@
],
"service": "default-whoami-80",
"rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)",
"priority": 47,
"status": "enabled",
"using": [
"web"

View file

@ -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 {

View file

@ -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 {

View file

@ -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",

View file

@ -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 {

View file

@ -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",

View file

@ -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 {

View file

@ -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",

386
pkg/api/sort.go Normal file
View file

@ -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)
})
}

1689
pkg/api/sort_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"
]
}
]

View file

@ -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"
]
}
]

View file

@ -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"
]
}
]

View file

@ -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
}
}
]

View file

@ -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"
]
}
]

View file

@ -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()
}

View file

@ -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)

View file

@ -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)
}
})
}
}

View file

@ -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,

View file

@ -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))
})
}
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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'],

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -6,9 +6,12 @@
<tr class="table-header">
<th
v-for="column in columns"
v-bind:class="`text-${column.align}`"
v-bind:key="column.name">
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 }}
<i v-if="currentSort === column.name" class="material-icons">{{currentSortDir === 'asc' ? 'arrow_drop_down' : 'arrow_drop_up'}}</i>
<i v-else style="opacity: 0" class="material-icons">{{currentSortDir === 'asc' ? 'arrow_drop_down' : 'arrow_drop_up'}}</i>
</th>
</tr>
</thead>
@ -27,9 +30,19 @@
v-bind:is="getColumn(column.name).component"
v-bind="getColumn(column.name).fieldToProps(row)"
>
<template v-if="getColumn(column.name).content">
<template v-if="getColumn(column.name).content && column.name !== 'priority'">
{{ getColumn(column.name).content(row) }}
</template>
<template v-if="getColumn(column.name).content && column.name === 'priority'">
<div>
{{ getColumn(column.name).content(row).short }}
</div>
<q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">
<div class="priority-tooltip">
{{ getColumn(column.name).content(row).long }}
</div>
</q-tooltip>
</template>
</component>
</td>
<td
@ -72,6 +85,12 @@ export default {
QSpinnerDots,
QPageScroller
},
data () {
return {
currentSort: 'name',
currentSortDir: 'asc'
}
},
methods: {
getColumn (columnName) {
return this.columns.find(c => c.name === columnName) || {}
@ -80,6 +99,14 @@ export default {
this.onLoadMore({ page: index })
.then(() => done())
.catch(() => done(true))
},
onSortClick (s) {
if (s === this.currentSort) {
this.currentSortDir = this.currentSortDir === 'asc' ? 'desc' : 'asc'
}
this.currentSort = s
this.$emit('update:currentSort', s)
this.$emit('update:currentSortDir', this.currentSortDir)
}
}
}
@ -127,4 +154,8 @@ export default {
font-size: 14px;
font-weight: 600;
}
.priority-tooltip{
font-size: larger;
}
</style>

View file

@ -73,6 +73,18 @@
</div>
</div>
</q-card-section>
<q-card-section v-if="data.priority">
<div class="row items-start no-wrap">
<div class="col">
<div class="text-subtitle2">PRIORITY</div>
<q-chip
dense
class="app-chip app-chip-entry-points">
{{ data.priority }}
</q-chip>
</div>
</div>
</q-card-section>
<q-card-section v-if="data.error">
<div class="row items-start no-wrap">
<div class="col">

View file

@ -44,12 +44,15 @@
<div class="row items-center q-col-gutter-lg">
<div class="col-12">
<main-table
:data="allRouters"
v-bind="getTableProps({ type: `${protocol}-routers` })"
:data="allRouters"
:onLoadMore="onGetAll"
:request="()=>{}"
:loading="routersLoading"
:pagination.sync="routersPagination"
:filter="routersFilter"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -91,7 +94,11 @@ export default {
page: 1,
rowsPerPage: 1000,
rowsNumber: 0
}
},
filter: '',
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -108,11 +115,14 @@ export default {
},
getRouterByName () {
return this[`${this.protocol}_getRouterByName`]
},
getAllRouters () {
return this[`${this.protocol}_getAllRouters`]
}
},
methods: {
...mapActions('http', { http_getMiddlewareByName: 'getMiddlewareByName', http_getRouterByName: 'getRouterByName' }),
...mapActions('tcp', { tcp_getMiddlewareByName: 'getMiddlewareByName', tcp_getRouterByName: 'getRouterByName' }),
...mapActions('http', { http_getMiddlewareByName: 'getMiddlewareByName', http_getRouterByName: 'getRouterByName', http_getAllRouters: 'getAllRouters' }),
...mapActions('tcp', { tcp_getMiddlewareByName: 'getMiddlewareByName', tcp_getRouterByName: 'getRouterByName', tcp_getAllRouters: 'getAllRouters' }),
refreshAll () {
if (this.middlewareByName.loading) {
return
@ -127,22 +137,26 @@ export default {
return
}
// Get routers
if (body.usedBy) {
for (const router in body.usedBy) {
if (body.usedBy.hasOwnProperty(router)) {
this.getRouterByName(body.usedBy[router])
.then(body => {
if (body) {
this.routersLoading = false
this.allRouters.push(body)
}
})
.catch(error => {
console.log('Error -> routers/byName', error)
})
this.getAllRouters({
query: this.filter,
status: this.status,
page: 1,
limit: 1000,
middlewareName: this.name,
serviceName: '',
sortBy: this.sortBy,
direction: this.sortDir
})
.then(body => {
this.allRouters = []
if (body) {
this.routersLoading = false
this.allRouters.push(...body.data)
}
}
}
})
.catch(error => {
console.log('Error -> routers/byName', error)
})
clearTimeout(this.timeOutGetAll)
this.timeOutGetAll = setTimeout(() => {
this.loading = false
@ -153,12 +167,18 @@ export default {
})
}
},
watch: {
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
created () {
this.refreshAll()
},
mounted () {
},
mounted () {},
beforeDestroy () {
clearInterval(this.timeOutGetAll)
this.$store.commit('http/getMiddlewareByNameClear')

View file

@ -112,12 +112,15 @@
<div class="row items-center q-col-gutter-lg">
<div class="col-12">
<main-table
:data="allRouters"
v-bind="getTableProps({ type: `${protocol}-routers` })"
:data="allRouters"
:onLoadMore="onGetAll"
:request="()=>{}"
:loading="routersLoading"
:pagination.sync="routersPagination"
:filter="routersFilter"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -167,7 +170,11 @@ export default {
page: 1,
rowsPerPage: 1000,
rowsNumber: 0
}
},
filter: '',
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -185,12 +192,15 @@ export default {
},
getRouterByName () {
return this[`${this.protocol}_getRouterByName`]
},
getAllRouters () {
return this[`${this.protocol}_getAllRouters`]
}
},
methods: {
...mapActions('http', { http_getServiceByName: 'getServiceByName', http_getRouterByName: 'getRouterByName' }),
...mapActions('tcp', { tcp_getServiceByName: 'getServiceByName', tcp_getRouterByName: 'getRouterByName' }),
...mapActions('udp', { udp_getServiceByName: 'getServiceByName', udp_getRouterByName: 'getRouterByName' }),
...mapActions('http', { http_getServiceByName: 'getServiceByName', http_getRouterByName: 'getRouterByName', http_getAllRouters: 'getAllRouters' }),
...mapActions('tcp', { tcp_getServiceByName: 'getServiceByName', tcp_getRouterByName: 'getRouterByName', tcp_getAllRouters: 'getAllRouters' }),
...mapActions('udp', { udp_getServiceByName: 'getServiceByName', udp_getRouterByName: 'getRouterByName', udp_getAllRouters: 'getAllRouters' }),
refreshAll () {
if (this.serviceByName.loading) {
return
@ -205,22 +215,26 @@ export default {
return
}
// Get routers
if (body.usedBy) {
for (const router in body.usedBy) {
if (body.usedBy.hasOwnProperty(router)) {
this.getRouterByName(body.usedBy[router])
.then(body => {
if (body) {
this.routersLoading = false
this.allRouters.push(body)
}
})
.catch(error => {
console.log('Error -> routers/byName', error)
})
this.getAllRouters({
query: this.filter,
status: this.status,
page: 1,
limit: 1000,
middlewareName: '',
serviceName: this.name,
sortBy: this.sortBy,
direction: this.sortDir
})
.then(body => {
this.allRouters = []
if (body) {
this.routersLoading = false
this.allRouters.push(...body.data)
}
}
}
})
.catch(error => {
console.log('Error -> getAllRouters', error)
})
clearTimeout(this.timeOutGetAll)
this.timeOutGetAll = setTimeout(() => {
this.loading = false
@ -231,12 +245,18 @@ export default {
})
}
},
watch: {
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
created () {
this.refreshAll()
},
mounted () {
},
mounted () {},
beforeDestroy () {
clearInterval(this.timeOutGetAll)
this.$store.commit('http/getServiceByNameClear')

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allMiddlewares.endReached"
:loading="allMiddlewares.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -62,6 +66,8 @@ export default {
return this.getAllMiddlewares({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +88,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allRouters.endReached"
:loading="allRouters.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -60,8 +64,12 @@ export default {
...mapActions('http', { getAllRouters: 'getAllRouters' }),
getAllRoutersWithParams (params) {
return this.getAllRouters({
serviceName: '',
middlewareName: '',
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +90,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allServices.endReached"
:loading="allServices.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -62,6 +66,8 @@ export default {
return this.getAllServices({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +88,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allMiddlewares.endReached"
:loading="allMiddlewares.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -62,6 +66,8 @@ export default {
return this.getAllMiddlewares({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +88,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allRouters.endReached"
:loading="allRouters.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -60,8 +64,12 @@ export default {
...mapActions('tcp', { getAllRouters: 'getAllRouters' }),
getAllRoutersWithParams (params) {
return this.getAllRouters({
serviceName: '',
middlewareName: '',
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +90,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allServices.endReached"
:loading="allServices.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -62,6 +66,8 @@ export default {
return this.getAllServices({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +88,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -14,6 +14,8 @@
:onLoadMore="handleLoadMore"
:endReached="allRouters.endReached"
:loading="allRouters.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -49,7 +51,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -61,6 +65,10 @@ export default {
return this.getAllRouters({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
serviceName: '',
middlewareName: '',
...params
})
},
@ -81,6 +89,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {

View file

@ -15,6 +15,8 @@
:onLoadMore="handleLoadMore"
:endReached="allServices.endReached"
:loading="allServices.loading"
:currentSort.sync="sortBy"
:currentSortDir.sync="sortDir"
/>
</div>
</div>
@ -50,7 +52,9 @@ export default {
data () {
return {
filter: '',
status: ''
status: '',
sortBy: 'name',
sortDir: 'asc'
}
},
computed: {
@ -62,6 +66,8 @@ export default {
return this.getAllServices({
query: this.filter,
status: this.status,
sortBy: this.sortBy,
direction: this.sortDir,
...params
})
},
@ -82,6 +88,12 @@ export default {
},
'filter' () {
this.refreshAll()
},
'sortBy' () {
this.refreshAll()
},
'sortDir' () {
this.refreshAll()
}
},
beforeDestroy () {