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/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/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)" > -