Improve API for the web UI

This commit is contained in:
Ludovic Fernandez 2019-09-02 11:38:04 +02:00 committed by Traefiker Bot
parent 17554202f6
commit af9762cf32
41 changed files with 1200 additions and 199 deletions

View file

@ -10,7 +10,10 @@
"tls": { "tls": {
"options": "default/mytlsoption" "options": "default/mytlsoption"
}, },
"status": "enabled" "status": "enabled",
"using": [
"web"
]
}, },
"default/test2.route-23c7f4c450289ee29016@kubernetescrd": { "default/test2.route-23c7f4c450289ee29016@kubernetescrd": {
"entryPoints": [ "entryPoints": [
@ -21,7 +24,10 @@
], ],
"service": "default/test2.route-23c7f4c450289ee29016", "service": "default/test2.route-23c7f4c450289ee29016",
"rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/tobestripped`)", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/tobestripped`)",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
} }
}, },
"middlewares": { "middlewares": {
@ -42,10 +48,10 @@
"loadBalancer": { "loadBalancer": {
"servers": [ "servers": [
{ {
"url": "http://10.42.0.4:80" "url": "http://10.42.0.2:80"
}, },
{ {
"url": "http://10.42.0.5:80" "url": "http://10.42.0.6:80"
} }
], ],
"passHostHeader": true "passHostHeader": true
@ -55,18 +61,18 @@
"default/test.route-6b204d94623b3df4370c@kubernetescrd" "default/test.route-6b204d94623b3df4370c@kubernetescrd"
], ],
"serverStatus": { "serverStatus": {
"http://10.42.0.4:80": "UP", "http://10.42.0.2:80": "UP",
"http://10.42.0.5:80": "UP" "http://10.42.0.6:80": "UP"
} }
}, },
"default/test2.route-23c7f4c450289ee29016@kubernetescrd": { "default/test2.route-23c7f4c450289ee29016@kubernetescrd": {
"loadBalancer": { "loadBalancer": {
"servers": [ "servers": [
{ {
"url": "http://10.42.0.4:80" "url": "http://10.42.0.2:80"
}, },
{ {
"url": "http://10.42.0.5:80" "url": "http://10.42.0.6:80"
} }
], ],
"passHostHeader": true "passHostHeader": true
@ -76,8 +82,8 @@
"default/test2.route-23c7f4c450289ee29016@kubernetescrd" "default/test2.route-23c7f4c450289ee29016@kubernetescrd"
], ],
"serverStatus": { "serverStatus": {
"http://10.42.0.4:80": "UP", "http://10.42.0.2:80": "UP",
"http://10.42.0.5:80": "UP" "http://10.42.0.6:80": "UP"
} }
} }
}, },
@ -92,7 +98,10 @@
"passthrough": false, "passthrough": false,
"options": "default/mytlsoption" "options": "default/mytlsoption"
}, },
"status": "enabled" "status": "enabled",
"using": [
"footcp"
]
} }
}, },
"tcpServices": { "tcpServices": {
@ -100,10 +109,10 @@
"loadBalancer": { "loadBalancer": {
"servers": [ "servers": [
{ {
"address": "10.42.0.3:8080" "address": "10.42.0.4:8080"
}, },
{ {
"address": "10.42.0.6:8080" "address": "10.42.0.5:8080"
} }
] ]
}, },

View file

@ -4,17 +4,29 @@
"service": "default/whoami/http", "service": "default/whoami/http",
"rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)",
"tls": {}, "tls": {},
"status": "enabled" "status": "enabled",
"using": [
"traefik",
"web"
]
}, },
"whoami-test-https/whoami@kubernetes": { "whoami-test-https/whoami@kubernetes": {
"service": "default/whoami/http", "service": "default/whoami/http",
"rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)",
"status": "enabled" "status": "enabled",
"using": [
"traefik",
"web"
]
}, },
"whoami-test/whoami@kubernetes": { "whoami-test/whoami@kubernetes": {
"service": "default/whoami/http", "service": "default/whoami/http",
"rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)",
"status": "enabled" "status": "enabled",
"using": [
"traefik",
"web"
]
} }
}, },
"services": { "services": {
@ -22,10 +34,10 @@
"loadBalancer": { "loadBalancer": {
"servers": [ "servers": [
{ {
"url": "http://10.42.0.4:80" "url": "http://10.42.0.2:80"
}, },
{ {
"url": "http://10.42.0.5:80" "url": "http://10.42.0.4:80"
} }
], ],
"passHostHeader": true "passHostHeader": true
@ -37,8 +49,8 @@
"whoami-test/whoami@kubernetes" "whoami-test/whoami@kubernetes"
], ],
"serverStatus": { "serverStatus": {
"http://10.42.0.4:80": "UP", "http://10.42.0.2:80": "UP",
"http://10.42.0.5:80": "UP" "http://10.42.0.4:80": "UP"
} }
} }
} }

102
pkg/api/criterion.go Normal file
View file

@ -0,0 +1,102 @@
package api
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
)
const (
defaultPerPage = 100
defaultPage = 1
)
const nextPageHeader = "X-Next-Page"
type pageInfo struct {
startIndex int
endIndex int
nextPage int
}
type searchCriterion struct {
Search string `url:"search"`
Status string `url:"status"`
}
func newSearchCriterion(query url.Values) *searchCriterion {
if len(query) == 0 {
return nil
}
search := query.Get("search")
status := query.Get("status")
if status == "" && search == "" {
return nil
}
return &searchCriterion{Search: search, Status: status}
}
func (c *searchCriterion) withStatus(name string) bool {
return c.Status == "" || strings.EqualFold(name, c.Status)
}
func (c *searchCriterion) searchIn(values ...string) bool {
if c.Search == "" {
return true
}
for _, v := range values {
if strings.Contains(strings.ToLower(v), strings.ToLower(c.Search)) {
return true
}
}
return false
}
func pagination(request *http.Request, max int) (pageInfo, error) {
perPage, err := getIntParam(request, "per_page", defaultPerPage)
if err != nil {
return pageInfo{}, err
}
page, err := getIntParam(request, "page", defaultPage)
if err != nil {
return pageInfo{}, err
}
startIndex := (page - 1) * perPage
if startIndex != 0 && startIndex >= max {
return pageInfo{}, fmt.Errorf("invalid request: page: %d, per_page: %d", page, perPage)
}
endIndex := startIndex + perPage
if endIndex >= max {
endIndex = max
}
nextPage := 1
if page*perPage < max {
nextPage = page + 1
}
return pageInfo{startIndex: startIndex, endIndex: endIndex, nextPage: nextPage}, nil
}
func getIntParam(request *http.Request, key string, defaultValue int) (int, error) {
raw := request.URL.Query().Get(key)
if raw == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return 0, fmt.Errorf("invalid request: %s: %d", key, value)
}
return value, nil
}

View file

@ -2,9 +2,8 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strconv" "reflect"
"strings" "strings"
"github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/runtime"
@ -15,12 +14,19 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
const ( type apiError struct {
defaultPerPage = 100 Message string `json:"message"`
defaultPage = 1 }
)
const nextPageHeader = "X-Next-Page" func writeError(rw http.ResponseWriter, msg string, code int) {
data, err := json.Marshal(apiError{Message: msg})
if err != nil {
http.Error(rw, msg, code)
return
}
http.Error(rw, string(data), code)
}
type serviceInfoRepresentation struct { type serviceInfoRepresentation struct {
*runtime.ServiceInfo *runtime.ServiceInfo
@ -36,12 +42,6 @@ type RunTimeRepresentation struct {
TCPServices map[string]*runtime.TCPServiceInfo `json:"tcpServices,omitempty"` TCPServices map[string]*runtime.TCPServiceInfo `json:"tcpServices,omitempty"`
} }
type pageInfo struct {
startIndex int
endIndex int
nextPage int
}
// Handler serves the configuration and status of Traefik on API endpoints. // Handler serves the configuration and status of Traefik on API endpoints.
type Handler struct { type Handler struct {
dashboard bool dashboard bool
@ -136,48 +136,19 @@ func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.R
} }
} }
func pagination(request *http.Request, max int) (pageInfo, error) {
perPage, err := getIntParam(request, "per_page", defaultPerPage)
if err != nil {
return pageInfo{}, err
}
page, err := getIntParam(request, "page", defaultPage)
if err != nil {
return pageInfo{}, err
}
startIndex := (page - 1) * perPage
if startIndex != 0 && startIndex >= max {
return pageInfo{}, fmt.Errorf("invalid request: page: %d, per_page: %d", page, perPage)
}
endIndex := startIndex + perPage
if endIndex >= max {
endIndex = max
}
nextPage := 1
if page*perPage < max {
nextPage = page + 1
}
return pageInfo{startIndex: startIndex, endIndex: endIndex, nextPage: nextPage}, nil
}
func getIntParam(request *http.Request, key string, defaultValue int) (int, error) {
raw := request.URL.Query().Get(key)
if raw == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return 0, fmt.Errorf("invalid request: %s: %d", key, value)
}
return value, nil
}
func getProviderName(id string) string { func getProviderName(id string) string {
return strings.SplitN(id, "@", 2)[1] return strings.SplitN(id, "@", 2)[1]
} }
func extractType(element interface{}) string {
v := reflect.ValueOf(element).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct {
if !field.IsNil() {
return v.Type().Field(i).Name
}
}
}
return ""
}

View file

@ -2,6 +2,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
@ -30,28 +31,31 @@ func (h Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) {
return results[i].Name < results[j].Name return results[i].Name < results[j].Name
}) })
rw.Header().Set("Content-Type", "application/json")
pageInfo, err := pagination(request, len(results)) pageInfo, err := pagination(request, len(results))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) writeError(rw, err.Error(), http.StatusBadRequest)
return return
} }
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) { func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) {
entryPointID := mux.Vars(request)["entryPointID"] entryPointID := mux.Vars(request)["entryPointID"]
rw.Header().Set("Content-Type", "application/json")
ep, ok := h.staticConfig.EntryPoints[entryPointID] ep, ok := h.staticConfig.EntryPoints[entryPointID]
if !ok { if !ok {
http.NotFound(rw, request) writeError(rw, fmt.Sprintf("entry point not found: %s", entryPointID), http.StatusNotFound)
return return
} }
@ -60,11 +64,9 @@ func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) {
Name: entryPointID, Name: entryPointID,
} }
rw.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }

View file

@ -2,9 +2,11 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
"strings"
"github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/runtime"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
@ -17,182 +19,224 @@ type routerRepresentation struct {
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
} }
func newRouterRepresentation(name string, rt *runtime.RouterInfo) routerRepresentation {
return routerRepresentation{
RouterInfo: rt,
Name: name,
Provider: getProviderName(name),
}
}
type serviceRepresentation struct { type serviceRepresentation struct {
*runtime.ServiceInfo *runtime.ServiceInfo
ServerStatus map[string]string `json:"serverStatus,omitempty"` ServerStatus map[string]string `json:"serverStatus,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
Type string `json:"type,omitempty"`
}
func newServiceRepresentation(name string, si *runtime.ServiceInfo) serviceRepresentation {
return serviceRepresentation{
ServiceInfo: si,
Name: name,
Provider: getProviderName(name),
ServerStatus: si.GetAllStatus(),
Type: strings.ToLower(extractType(si.Service)),
}
} }
type middlewareRepresentation struct { type middlewareRepresentation struct {
*runtime.MiddlewareInfo *runtime.MiddlewareInfo
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
Type string `json:"type,omitempty"`
}
func newMiddlewareRepresentation(name string, mi *runtime.MiddlewareInfo) middlewareRepresentation {
return middlewareRepresentation{
MiddlewareInfo: mi,
Name: name,
Provider: getProviderName(name),
Type: strings.ToLower(extractType(mi.Middleware)),
}
} }
func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) { func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) {
results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers)) results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers))
criterion := newSearchCriterion(request.URL.Query())
for name, rt := range h.runtimeConfiguration.Routers { for name, rt := range h.runtimeConfiguration.Routers {
results = append(results, routerRepresentation{ if keepRouter(name, rt, criterion) {
RouterInfo: rt, results = append(results, newRouterRepresentation(name, rt))
Name: name, }
Provider: getProviderName(name),
})
} }
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name return results[i].Name < results[j].Name
}) })
rw.Header().Set("Content-Type", "application/json")
pageInfo, err := pagination(request, len(results)) pageInfo, err := pagination(request, len(results))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) writeError(rw, err.Error(), http.StatusBadRequest)
return return
} }
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) { func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) {
routerID := mux.Vars(request)["routerID"] routerID := mux.Vars(request)["routerID"]
rw.Header().Set("Content-Type", "application/json")
router, ok := h.runtimeConfiguration.Routers[routerID] router, ok := h.runtimeConfiguration.Routers[routerID]
if !ok { if !ok {
http.NotFound(rw, request) writeError(rw, fmt.Sprintf("router not found: %s", routerID), http.StatusNotFound)
return return
} }
result := routerRepresentation{ result := newRouterRepresentation(routerID, router)
RouterInfo: router,
Name: routerID,
Provider: getProviderName(routerID),
}
rw.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) { func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) {
results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services)) results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services))
criterion := newSearchCriterion(request.URL.Query())
for name, si := range h.runtimeConfiguration.Services { for name, si := range h.runtimeConfiguration.Services {
results = append(results, serviceRepresentation{ if keepService(name, si, criterion) {
ServiceInfo: si, results = append(results, newServiceRepresentation(name, si))
Name: name, }
Provider: getProviderName(name),
ServerStatus: si.GetAllStatus(),
})
} }
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name return results[i].Name < results[j].Name
}) })
rw.Header().Set("Content-Type", "application/json")
pageInfo, err := pagination(request, len(results)) pageInfo, err := pagination(request, len(results))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) writeError(rw, err.Error(), http.StatusBadRequest)
return return
} }
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getService(rw http.ResponseWriter, request *http.Request) { func (h Handler) getService(rw http.ResponseWriter, request *http.Request) {
serviceID := mux.Vars(request)["serviceID"] serviceID := mux.Vars(request)["serviceID"]
rw.Header().Add("Content-Type", "application/json")
service, ok := h.runtimeConfiguration.Services[serviceID] service, ok := h.runtimeConfiguration.Services[serviceID]
if !ok { if !ok {
http.NotFound(rw, request) writeError(rw, fmt.Sprintf("service not found: %s", serviceID), http.StatusNotFound)
return return
} }
result := serviceRepresentation{ result := newServiceRepresentation(serviceID, service)
ServiceInfo: service,
Name: serviceID,
Provider: getProviderName(serviceID),
ServerStatus: service.GetAllStatus(),
}
rw.Header().Add("Content-Type", "application/json")
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) { func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) {
results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares)) results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares))
criterion := newSearchCriterion(request.URL.Query())
for name, mi := range h.runtimeConfiguration.Middlewares { for name, mi := range h.runtimeConfiguration.Middlewares {
results = append(results, middlewareRepresentation{ if keepMiddleware(name, mi, criterion) {
MiddlewareInfo: mi, results = append(results, newMiddlewareRepresentation(name, mi))
Name: name, }
Provider: getProviderName(name),
})
} }
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name return results[i].Name < results[j].Name
}) })
rw.Header().Set("Content-Type", "application/json")
pageInfo, err := pagination(request, len(results)) pageInfo, err := pagination(request, len(results))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) writeError(rw, err.Error(), http.StatusBadRequest)
return return
} }
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) { func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) {
middlewareID := mux.Vars(request)["middlewareID"] middlewareID := mux.Vars(request)["middlewareID"]
rw.Header().Set("Content-Type", "application/json")
middleware, ok := h.runtimeConfiguration.Middlewares[middlewareID] middleware, ok := h.runtimeConfiguration.Middlewares[middlewareID]
if !ok { if !ok {
http.NotFound(rw, request) writeError(rw, fmt.Sprintf("middleware not found: %s", middlewareID), http.StatusNotFound)
return return
} }
result := middlewareRepresentation{ result := newMiddlewareRepresentation(middlewareID, middleware)
MiddlewareInfo: middleware,
Name: middlewareID,
Provider: getProviderName(middlewareID),
}
rw.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func keepRouter(name string, item *runtime.RouterInfo, criterion *searchCriterion) bool {
if criterion == nil {
return true
}
return criterion.withStatus(item.Status) && criterion.searchIn(item.Rule, name)
}
func keepService(name string, item *runtime.ServiceInfo, criterion *searchCriterion) bool {
if criterion == nil {
return true
}
return criterion.withStatus(item.Status) && criterion.searchIn(name)
}
func keepMiddleware(name string, item *runtime.MiddlewareInfo, criterion *searchCriterion) bool {
if criterion == nil {
return true
}
return criterion.withStatus(item.Status) && criterion.searchIn(name)
}

View file

@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -137,6 +138,68 @@ func TestHandler_HTTP(t *testing.T) {
statusCode: http.StatusBadRequest, statusCode: http.StatusBadRequest,
}, },
}, },
{
desc: "routers filtered by status",
path: "/api/http/routers?status=enabled",
conf: runtime.Configuration{
Routers: map[string]*runtime.RouterInfo{
"test@myprovider": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Service: "foo-service@myprovider",
Rule: "Host(`foo.bar.other`)",
Middlewares: []string{"addPrefixTest", "auth"},
},
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-status.json",
},
},
{
desc: "routers filtered by search",
path: "/api/http/routers?search=fii",
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,
},
"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-search.json",
},
},
{ {
desc: "one router by id", desc: "one router by id",
path: "/api/http/routers/bar@myprovider", path: "/api/http/routers/bar@myprovider",
@ -232,6 +295,45 @@ func TestHandler_HTTP(t *testing.T) {
si.UpdateServerStatus("http://127.0.0.2", "UP") si.UpdateServerStatus("http://127.0.0.2", "UP")
return si return si
}(), }(),
"canary@myprovider": {
Service: &dynamic.Service{
Weighted: &dynamic.WeightedRoundRobin{
Services: nil,
Sticky: &dynamic.Sticky{
Cookie: &dynamic.Cookie{
Name: "chocolat",
Secure: true,
HTTPOnly: true,
},
},
},
},
Status: runtime.StatusEnabled,
UsedBy: []string{"foo@myprovider"},
},
"mirror@myprovider": {
Service: &dynamic.Service{
Mirroring: &dynamic.Mirroring{
Service: "one@myprovider",
Mirrors: []dynamic.MirrorService{
{
Name: "two@myprovider",
Percent: 10,
},
{
Name: "three@myprovider",
Percent: 15,
},
{
Name: "four@myprovider",
Percent: 80,
},
},
},
},
Status: runtime.StatusEnabled,
UsedBy: []string{"foo@myprovider"},
},
}, },
}, },
expected: expected{ expected: expected{
@ -301,6 +403,100 @@ func TestHandler_HTTP(t *testing.T) {
jsonFile: "testdata/services-page2.json", jsonFile: "testdata/services-page2.json",
}, },
}, },
{
desc: "services filtered by status",
path: "/api/http/services?status=enabled",
conf: runtime.Configuration{
Services: map[string]*runtime.ServiceInfo{
"bar@myprovider": func() *runtime.ServiceInfo {
si := &runtime.ServiceInfo{
Service: &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
UsedBy: []string{"foo@myprovider", "test@myprovider"},
Status: runtime.StatusEnabled,
}
si.UpdateServerStatus("http://127.0.0.1", "UP")
return si
}(),
"baz@myprovider": func() *runtime.ServiceInfo {
si := &runtime.ServiceInfo{
Service: &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.2",
},
},
},
},
UsedBy: []string{"foo@myprovider"},
Status: runtime.StatusDisabled,
}
si.UpdateServerStatus("http://127.0.0.2", "UP")
return si
}(),
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/services-filtered-status.json",
},
},
{
desc: "services filtered by search",
path: "/api/http/services?search=baz",
conf: runtime.Configuration{
Services: map[string]*runtime.ServiceInfo{
"bar@myprovider": func() *runtime.ServiceInfo {
si := &runtime.ServiceInfo{
Service: &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
UsedBy: []string{"foo@myprovider", "test@myprovider"},
Status: runtime.StatusEnabled,
}
si.UpdateServerStatus("http://127.0.0.1", "UP")
return si
}(),
"baz@myprovider": func() *runtime.ServiceInfo {
si := &runtime.ServiceInfo{
Service: &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.2",
},
},
},
},
UsedBy: []string{"foo@myprovider"},
Status: runtime.StatusDisabled,
}
si.UpdateServerStatus("http://127.0.0.2", "UP")
return si
}(),
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/services-filtered-search.json",
},
},
{ {
desc: "one service by id", desc: "one service by id",
path: "/api/http/services/bar@myprovider", path: "/api/http/services/bar@myprovider",
@ -411,6 +607,86 @@ func TestHandler_HTTP(t *testing.T) {
jsonFile: "testdata/middlewares.json", jsonFile: "testdata/middlewares.json",
}, },
}, },
{
desc: "middlewares filtered by status",
path: "/api/http/middlewares?status=enabled",
conf: runtime.Configuration{
Middlewares: map[string]*runtime.MiddlewareInfo{
"auth@myprovider": {
Middleware: &dynamic.Middleware{
BasicAuth: &dynamic.BasicAuth{
Users: []string{"admin:admin"},
},
},
UsedBy: []string{"bar@myprovider", "test@myprovider"},
Status: runtime.StatusEnabled,
},
"addPrefixTest@myprovider": {
Middleware: &dynamic.Middleware{
AddPrefix: &dynamic.AddPrefix{
Prefix: "/titi",
},
},
UsedBy: []string{"test@myprovider"},
Status: runtime.StatusDisabled,
},
"addPrefixTest@anotherprovider": {
Middleware: &dynamic.Middleware{
AddPrefix: &dynamic.AddPrefix{
Prefix: "/toto",
},
},
UsedBy: []string{"bar@myprovider"},
Status: runtime.StatusEnabled,
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/middlewares-filtered-status.json",
},
},
{
desc: "middlewares filtered by search",
path: "/api/http/middlewares?search=addprefixtest",
conf: runtime.Configuration{
Middlewares: map[string]*runtime.MiddlewareInfo{
"auth@myprovider": {
Middleware: &dynamic.Middleware{
BasicAuth: &dynamic.BasicAuth{
Users: []string{"admin:admin"},
},
},
UsedBy: []string{"bar@myprovider", "test@myprovider"},
Status: runtime.StatusEnabled,
},
"addPrefixTest@myprovider": {
Middleware: &dynamic.Middleware{
AddPrefix: &dynamic.AddPrefix{
Prefix: "/titi",
},
},
UsedBy: []string{"test@myprovider"},
Status: runtime.StatusDisabled,
},
"addPrefixTest@anotherprovider": {
Middleware: &dynamic.Middleware{
AddPrefix: &dynamic.AddPrefix{
Prefix: "/toto",
},
},
UsedBy: []string{"bar@myprovider"},
Status: runtime.StatusEnabled,
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/middlewares-filtered-search.json",
},
},
{ {
desc: "all middlewares, 1 res per page, want page 2", desc: "all middlewares, 1 res per page, want page 2",
path: "/api/http/middlewares?page=2&per_page=1", path: "/api/http/middlewares?page=2&per_page=1",
@ -521,6 +797,8 @@ func TestHandler_HTTP(t *testing.T) {
rtConf := &test.conf rtConf := &test.conf
// To lazily initialize the Statuses. // To lazily initialize the Statuses.
rtConf.PopulateUsedBy() rtConf.PopulateUsedBy()
rtConf.GetRoutersByEntryPoints(context.Background(), []string{"web"}, false)
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
router := mux.NewRouter() router := mux.NewRouter()
handler.Append(router) handler.Append(router)

View file

@ -56,7 +56,7 @@ func (h Handler) getOverview(rw http.ResponseWriter, request *http.Request) {
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }

View file

@ -2,9 +2,11 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
"strings"
"github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/runtime"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
@ -17,118 +19,146 @@ type tcpRouterRepresentation struct {
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
} }
func newTCPRouterRepresentation(name string, rt *runtime.TCPRouterInfo) tcpRouterRepresentation {
return tcpRouterRepresentation{
TCPRouterInfo: rt,
Name: name,
Provider: getProviderName(name),
}
}
type tcpServiceRepresentation struct { type tcpServiceRepresentation struct {
*runtime.TCPServiceInfo *runtime.TCPServiceInfo
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
Type string `json:"type,omitempty"`
}
func newTCPServiceRepresentation(name string, si *runtime.TCPServiceInfo) tcpServiceRepresentation {
return tcpServiceRepresentation{
TCPServiceInfo: si,
Name: name,
Provider: getProviderName(name),
Type: strings.ToLower(extractType(si.TCPService)),
}
} }
func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) { func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) {
results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters)) results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters))
criterion := newSearchCriterion(request.URL.Query())
for name, rt := range h.runtimeConfiguration.TCPRouters { for name, rt := range h.runtimeConfiguration.TCPRouters {
results = append(results, tcpRouterRepresentation{ if keepTCPRouter(name, rt, criterion) {
TCPRouterInfo: rt, results = append(results, newTCPRouterRepresentation(name, rt))
Name: name, }
Provider: getProviderName(name),
})
} }
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name return results[i].Name < results[j].Name
}) })
rw.Header().Set("Content-Type", "application/json")
pageInfo, err := pagination(request, len(results)) pageInfo, err := pagination(request, len(results))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) writeError(rw, err.Error(), http.StatusBadRequest)
return return
} }
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) { func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) {
routerID := mux.Vars(request)["routerID"] routerID := mux.Vars(request)["routerID"]
rw.Header().Set("Content-Type", "application/json")
router, ok := h.runtimeConfiguration.TCPRouters[routerID] router, ok := h.runtimeConfiguration.TCPRouters[routerID]
if !ok { if !ok {
http.NotFound(rw, request) writeError(rw, fmt.Sprintf("router not found: %s", routerID), http.StatusNotFound)
return return
} }
result := tcpRouterRepresentation{ result := newTCPRouterRepresentation(routerID, router)
TCPRouterInfo: router,
Name: routerID,
Provider: getProviderName(routerID),
}
rw.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) { func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) {
results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices)) results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices))
criterion := newSearchCriterion(request.URL.Query())
for name, si := range h.runtimeConfiguration.TCPServices { for name, si := range h.runtimeConfiguration.TCPServices {
results = append(results, tcpServiceRepresentation{ if keepTCPService(name, si, criterion) {
TCPServiceInfo: si, results = append(results, newTCPServiceRepresentation(name, si))
Name: name, }
Provider: getProviderName(name),
})
} }
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name return results[i].Name < results[j].Name
}) })
rw.Header().Set("Content-Type", "application/json")
pageInfo, err := pagination(request, len(results)) pageInfo, err := pagination(request, len(results))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) writeError(rw, err.Error(), http.StatusBadRequest)
return return
} }
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage))
err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex])
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) { func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) {
serviceID := mux.Vars(request)["serviceID"] serviceID := mux.Vars(request)["serviceID"]
rw.Header().Set("Content-Type", "application/json")
service, ok := h.runtimeConfiguration.TCPServices[serviceID] service, ok := h.runtimeConfiguration.TCPServices[serviceID]
if !ok { if !ok {
http.NotFound(rw, request) writeError(rw, fmt.Sprintf("service not found: %s", serviceID), http.StatusNotFound)
return return
} }
result := tcpServiceRepresentation{ result := newTCPServiceRepresentation(serviceID, service)
TCPServiceInfo: service,
Name: serviceID,
Provider: getProviderName(serviceID),
}
rw.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(rw).Encode(result) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) writeError(rw, err.Error(), http.StatusInternalServerError)
} }
} }
func keepTCPRouter(name string, item *runtime.TCPRouterInfo, criterion *searchCriterion) bool {
if criterion == nil {
return true
}
return criterion.withStatus(item.Status) && criterion.searchIn(item.Rule, name)
}
func keepTCPService(name string, item *runtime.TCPServiceInfo, criterion *searchCriterion) bool {
if criterion == nil {
return true
}
return criterion.withStatus(item.Status) && criterion.searchIn(name)
}

View file

@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -112,6 +113,86 @@ func TestHandler_TCP(t *testing.T) {
jsonFile: "testdata/tcprouters-page2.json", jsonFile: "testdata/tcprouters-page2.json",
}, },
}, },
{
desc: "TCP routers filtered by status",
path: "/api/tcp/routers?status=enabled",
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@myprovider",
Rule: "Host(`foo.bar`)",
},
Status: runtime.StatusWarning,
},
"foo@myprovider": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service@myprovider",
Rule: "Host(`foo.bar`)",
},
Status: runtime.StatusDisabled,
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/tcprouters-filtered-status.json",
},
},
{
desc: "TCP routers filtered by search",
path: "/api/tcp/routers?search=bar@my",
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@myprovider",
Rule: "Host(`foo.bar`)",
},
Status: runtime.StatusWarning,
},
"foo@myprovider": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service@myprovider",
Rule: "Host(`foo.bar`)",
},
Status: runtime.StatusDisabled,
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/tcprouters-filtered-search.json",
},
},
{ {
desc: "one TCP router by id", desc: "one TCP router by id",
path: "/api/tcp/routers/bar@myprovider", path: "/api/tcp/routers/bar@myprovider",
@ -219,6 +300,110 @@ func TestHandler_TCP(t *testing.T) {
jsonFile: "testdata/tcpservices.json", jsonFile: "testdata/tcpservices.json",
}, },
}, },
{
desc: "tcp services filtered by status",
path: "/api/tcp/services?status=enabled",
conf: runtime.Configuration{
TCPServices: map[string]*runtime.TCPServiceInfo{
"bar@myprovider": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.1:2345",
},
},
},
},
UsedBy: []string{"foo@myprovider", "test@myprovider"},
Status: runtime.StatusEnabled,
},
"baz@myprovider": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.2:2345",
},
},
},
},
UsedBy: []string{"foo@myprovider"},
Status: runtime.StatusWarning,
},
"foz@myprovider": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.2:2345",
},
},
},
},
UsedBy: []string{"foo@myprovider"},
Status: runtime.StatusDisabled,
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/tcpservices-filtered-status.json",
},
},
{
desc: "tcp services filtered by search",
path: "/api/tcp/services?search=baz@my",
conf: runtime.Configuration{
TCPServices: map[string]*runtime.TCPServiceInfo{
"bar@myprovider": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.1:2345",
},
},
},
},
UsedBy: []string{"foo@myprovider", "test@myprovider"},
Status: runtime.StatusEnabled,
},
"baz@myprovider": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.2:2345",
},
},
},
},
UsedBy: []string{"foo@myprovider"},
Status: runtime.StatusWarning,
},
"foz@myprovider": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.2:2345",
},
},
},
},
UsedBy: []string{"foo@myprovider"},
Status: runtime.StatusDisabled,
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/tcpservices-filtered-search.json",
},
},
{ {
desc: "all tcp services, 1 res per page, want page 2", desc: "all tcp services, 1 res per page, want page 2",
path: "/api/tcp/services?page=2&per_page=1", path: "/api/tcp/services?page=2&per_page=1",
@ -330,6 +515,10 @@ func TestHandler_TCP(t *testing.T) {
t.Parallel() t.Parallel()
rtConf := &test.conf rtConf := &test.conf
// To lazily initialize the Statuses.
rtConf.PopulateUsedBy()
rtConf.GetTCPRoutersByEntryPoints(context.Background(), []string{"web"})
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
router := mux.NewRouter() router := mux.NewRouter()
handler.Append(router) handler.Append(router)

View file

@ -7,6 +7,7 @@
"name": "auth@myprovider", "name": "auth@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled", "status": "enabled",
"type": "basicauth",
"usedBy": [ "usedBy": [
"bar@myprovider", "bar@myprovider",
"test@myprovider" "test@myprovider"

View file

@ -0,0 +1,26 @@
[
{
"addPrefix": {
"prefix": "/toto"
},
"name": "addPrefixTest@anotherprovider",
"provider": "anotherprovider",
"status": "enabled",
"type": "addprefix",
"usedBy": [
"bar@myprovider"
]
},
{
"addPrefix": {
"prefix": "/titi"
},
"name": "addPrefixTest@myprovider",
"provider": "myprovider",
"status": "disabled",
"type": "addprefix",
"usedBy": [
"test@myprovider"
]
}
]

View file

@ -0,0 +1,29 @@
[
{
"addPrefix": {
"prefix": "/toto"
},
"name": "addPrefixTest@anotherprovider",
"provider": "anotherprovider",
"status": "enabled",
"type": "addprefix",
"usedBy": [
"bar@myprovider"
]
},
{
"basicAuth": {
"users": [
"admin:admin"
]
},
"name": "auth@myprovider",
"provider": "myprovider",
"status": "enabled",
"type": "basicauth",
"usedBy": [
"bar@myprovider",
"test@myprovider"
]
}
]

View file

@ -6,6 +6,7 @@
"name": "addPrefixTest@myprovider", "name": "addPrefixTest@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled", "status": "enabled",
"type": "addprefix",
"usedBy": [ "usedBy": [
"test@myprovider" "test@myprovider"
] ]

View file

@ -6,6 +6,7 @@
"name": "addPrefixTest@anotherprovider", "name": "addPrefixTest@anotherprovider",
"provider": "anotherprovider", "provider": "anotherprovider",
"status": "enabled", "status": "enabled",
"type": "addprefix",
"usedBy": [ "usedBy": [
"bar@myprovider" "bar@myprovider"
] ]
@ -17,6 +18,7 @@
"name": "addPrefixTest@myprovider", "name": "addPrefixTest@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled", "status": "enabled",
"type": "addprefix",
"usedBy": [ "usedBy": [
"test@myprovider" "test@myprovider"
] ]
@ -30,6 +32,7 @@
"name": "auth@myprovider", "name": "auth@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled", "status": "enabled",
"type": "basicauth",
"usedBy": [ "usedBy": [
"bar@myprovider", "bar@myprovider",
"test@myprovider" "test@myprovider"

View file

@ -10,5 +10,8 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar`)", "rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
} }

View file

@ -0,0 +1,19 @@
[
{
"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,19 @@
[
{
"entryPoints": [
"web"
],
"middlewares": [
"addPrefixTest",
"auth"
],
"name": "test@myprovider",
"provider": "myprovider",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
"using": [
"web"
]
}
]

View file

@ -7,7 +7,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar14`)", "rule": "Host(`foo.bar14`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -17,7 +20,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar15`)", "rule": "Host(`foo.bar15`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -27,7 +33,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar16`)", "rule": "Host(`foo.bar16`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -37,7 +46,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar17`)", "rule": "Host(`foo.bar17`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -47,6 +59,9 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar18`)", "rule": "Host(`foo.bar18`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
} }
] ]

View file

@ -7,6 +7,9 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`toto.bar`)", "rule": "Host(`toto.bar`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
} }
] ]

View file

@ -11,7 +11,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar`)", "rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -25,6 +28,9 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar.other`)", "rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "enabled" "status": "enabled",
"using": [
"web"
]
} }
] ]

View file

@ -13,6 +13,7 @@
"http://127.0.0.1": "UP" "http://127.0.0.1": "UP"
}, },
"status": "enabled", "status": "enabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider", "foo@myprovider",
"test@myprovider" "test@myprovider"

View file

@ -0,0 +1,22 @@
[
{
"loadBalancer": {
"passHostHeader": false,
"servers": [
{
"url": "http://127.0.0.2"
}
]
},
"name": "baz@myprovider",
"provider": "myprovider",
"serverStatus": {
"http://127.0.0.2": "UP"
},
"status": "disabled",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider"
]
}
]

View file

@ -0,0 +1,23 @@
[
{
"loadBalancer": {
"passHostHeader": false,
"servers": [
{
"url": "http://127.0.0.1"
}
]
},
"name": "bar@myprovider",
"provider": "myprovider",
"serverStatus": {
"http://127.0.0.1": "UP"
},
"status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider",
"test@myprovider"
]
}
]

View file

@ -14,6 +14,7 @@
"http://127.0.0.2": "UP" "http://127.0.0.2": "UP"
}, },
"status": "enabled", "status": "enabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider" "foo@myprovider"
] ]

View file

@ -14,6 +14,7 @@
"http://127.0.0.1": "UP" "http://127.0.0.1": "UP"
}, },
"status": "enabled", "status": "enabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider", "foo@myprovider",
"test@myprovider" "test@myprovider"
@ -34,6 +35,51 @@
"http://127.0.0.2": "UP" "http://127.0.0.2": "UP"
}, },
"status": "enabled", "status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider"
]
},
{
"name": "canary@myprovider",
"provider": "myprovider",
"status": "enabled",
"type": "weighted",
"usedBy": [
"foo@myprovider"
],
"weighted": {
"sticky": {
"cookie": {
"httpOnly": true,
"name": "chocolat",
"secure": true
}
}
}
},
{
"mirroring": {
"mirrors": [
{
"name": "two@myprovider",
"percent": 10
},
{
"name": "three@myprovider",
"percent": 15
},
{
"name": "four@myprovider",
"percent": 80
}
],
"service": "one@myprovider"
},
"name": "mirror@myprovider",
"provider": "myprovider",
"status": "enabled",
"type": "mirroring",
"usedBy": [ "usedBy": [
"foo@myprovider" "foo@myprovider"
] ]

View file

@ -5,5 +5,9 @@
"name": "bar@myprovider", "name": "bar@myprovider",
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar`)", "rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider" "service": "foo-service@myprovider",
"status": "enabled",
"using": [
"web"
]
} }

View file

@ -0,0 +1,15 @@
[
{
"entryPoints": [
"web"
],
"name": "bar@myprovider",
"provider": "myprovider",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "warning",
"using": [
"web"
]
}
]

View file

@ -0,0 +1,18 @@
[
{
"entryPoints": [
"web"
],
"name": "test@myprovider",
"provider": "myprovider",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
"tls": {
"passthrough": false
},
"using": [
"web"
]
}
]

View file

@ -6,6 +6,10 @@
"name": "baz@myprovider", "name": "baz@myprovider",
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`toto.bar`)", "rule": "Host(`toto.bar`)",
"service": "foo-service@myprovider" "service": "foo-service@myprovider",
"status": "enabled",
"using": [
"web"
]
} }
] ]

View file

@ -7,7 +7,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar`)", "rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "warning" "status": "warning",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -17,7 +20,10 @@
"provider": "myprovider", "provider": "myprovider",
"rule": "Host(`foo.bar`)", "rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider", "service": "foo-service@myprovider",
"status": "disabled" "status": "disabled",
"using": [
"web"
]
}, },
{ {
"entryPoints": [ "entryPoints": [
@ -30,6 +36,9 @@
"status": "enabled", "status": "enabled",
"tls": { "tls": {
"passthrough": false "passthrough": false
} },
"using": [
"web"
]
} }
] ]

View file

@ -8,6 +8,8 @@
}, },
"name": "bar@myprovider", "name": "bar@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider", "foo@myprovider",
"test@myprovider" "test@myprovider"

View file

@ -0,0 +1,18 @@
[
{
"loadBalancer": {
"servers": [
{
"address": "127.0.0.2:2345"
}
]
},
"name": "baz@myprovider",
"provider": "myprovider",
"status": "warning",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider"
]
}
]

View file

@ -0,0 +1,19 @@
[
{
"loadBalancer": {
"servers": [
{
"address": "127.0.0.1:2345"
}
]
},
"name": "bar@myprovider",
"provider": "myprovider",
"status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider",
"test@myprovider"
]
}
]

View file

@ -9,6 +9,8 @@
}, },
"name": "baz@myprovider", "name": "baz@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider" "foo@myprovider"
] ]

View file

@ -10,6 +10,7 @@
"name": "bar@myprovider", "name": "bar@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "enabled", "status": "enabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider", "foo@myprovider",
"test@myprovider" "test@myprovider"
@ -26,6 +27,7 @@
"name": "baz@myprovider", "name": "baz@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "warning", "status": "warning",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider" "foo@myprovider"
] ]
@ -41,6 +43,7 @@
"name": "foz@myprovider", "name": "foz@myprovider",
"provider": "myprovider", "provider": "myprovider",
"status": "disabled", "status": "disabled",
"type": "loadbalancer",
"usedBy": [ "usedBy": [
"foo@myprovider" "foo@myprovider"
] ]

View file

@ -3,13 +3,14 @@ package runtime
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"sync" "sync"
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
) )
// GetRoutersByEntryPoints returns all the http routers by entry points name and routers name // GetRoutersByEntryPoints returns all the http routers by entry points name and routers name.
func (c *Configuration) GetRoutersByEntryPoints(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*RouterInfo { func (c *Configuration) GetRoutersByEntryPoints(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*RouterInfo {
entryPointsRouters := make(map[string]map[string]*RouterInfo) entryPointsRouters := make(map[string]map[string]*RouterInfo)
@ -19,11 +20,13 @@ func (c *Configuration) GetRoutersByEntryPoints(ctx context.Context, entryPoints
} }
logger := log.FromContext(log.With(ctx, log.Str(log.RouterName, rtName))) logger := log.FromContext(log.With(ctx, log.Str(log.RouterName, rtName)))
eps := rt.EntryPoints eps := rt.EntryPoints
if len(eps) == 0 { if len(eps) == 0 {
logger.Debugf("No entrypoint defined for this router, using the default one(s) instead: %+v", entryPoints) logger.Debugf("No entryPoint defined for this router, using the default one(s) instead: %+v", entryPoints)
eps = entryPoints eps = entryPoints
} }
entryPointsCount := 0 entryPointsCount := 0
for _, entryPointName := range eps { for _, entryPointName := range eps {
if !contains(entryPoints, entryPointName) { if !contains(entryPoints, entryPointName) {
@ -33,23 +36,44 @@ func (c *Configuration) GetRoutersByEntryPoints(ctx context.Context, entryPoints
continue continue
} }
entryPointsCount++
if _, ok := entryPointsRouters[entryPointName]; !ok { if _, ok := entryPointsRouters[entryPointName]; !ok {
entryPointsRouters[entryPointName] = make(map[string]*RouterInfo) entryPointsRouters[entryPointName] = make(map[string]*RouterInfo)
} }
entryPointsCount++
rt.Using = append(rt.Using, entryPointName)
entryPointsRouters[entryPointName][rtName] = rt entryPointsRouters[entryPointName][rtName] = rt
} }
if entryPointsCount == 0 { if entryPointsCount == 0 {
rt.AddError(fmt.Errorf("no valid entryPoint for this router"), true) rt.AddError(fmt.Errorf("no valid entryPoint for this router"), true)
logger.Error("no valid entryPoint for this router") logger.Error("no valid entryPoint for this router")
} }
rt.Using = unique(rt.Using)
} }
return entryPointsRouters return entryPointsRouters
} }
// RouterInfo holds information about a currently running HTTP router func unique(src []string) []string {
var uniq []string
set := make(map[string]struct{})
for _, v := range src {
if _, exist := set[v]; !exist {
set[v] = struct{}{}
uniq = append(uniq, v)
}
}
sort.Strings(uniq)
return uniq
}
// RouterInfo holds information about a currently running HTTP router.
type RouterInfo struct { type RouterInfo struct {
*dynamic.Router // dynamic configuration *dynamic.Router // dynamic configuration
// Err contains all the errors that occurred during router's creation. // Err contains all the errors that occurred during router's creation.
@ -57,7 +81,8 @@ type RouterInfo struct {
// Status reports whether the router is disabled, in a warning state, or all good (enabled). // Status reports whether the router is disabled, in a warning state, or all good (enabled).
// If not in "enabled" state, the reason for it should be in the list of Err. // If not in "enabled" state, the reason for it should be in the list of Err.
// It is the caller's responsibility to set the initial status. // It is the caller's responsibility to set the initial status.
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Using []string `json:"using,omitempty"` // Effective entry points used by that router.
} }
// AddError adds err to r.Err, if it does not already exist. // AddError adds err to r.Err, if it does not already exist.
@ -81,13 +106,13 @@ func (r *RouterInfo) AddError(err error, critical bool) {
} }
} }
// MiddlewareInfo holds information about a currently running middleware // MiddlewareInfo holds information about a currently running middleware.
type MiddlewareInfo struct { type MiddlewareInfo struct {
*dynamic.Middleware // dynamic configuration *dynamic.Middleware // dynamic configuration
// Err contains all the errors that occurred during service creation. // Err contains all the errors that occurred during service creation.
Err []string `json:"error,omitempty"` Err []string `json:"error,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
UsedBy []string `json:"usedBy,omitempty"` // list of routers and services using that middleware UsedBy []string `json:"usedBy,omitempty"` // list of routers and services using that middleware.
} }
// AddError adds err to s.Err, if it does not already exist. // AddError adds err to s.Err, if it does not already exist.
@ -111,7 +136,7 @@ func (m *MiddlewareInfo) AddError(err error, critical bool) {
} }
} }
// ServiceInfo holds information about a currently running service // ServiceInfo holds information about a currently running service.
type ServiceInfo struct { type ServiceInfo struct {
*dynamic.Service // dynamic configuration *dynamic.Service // dynamic configuration
// Err contains all the errors that occurred during service creation. // Err contains all the errors that occurred during service creation.

View file

@ -104,6 +104,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) {
Rule: "Host(`bar.foo`)", Rule: "Host(`bar.foo`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web"},
}, },
"foobar": { "foobar": {
Router: &dynamic.Router{ Router: &dynamic.Router{
@ -113,6 +114,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) {
}, },
Status: "warning", Status: "warning",
Err: []string{`entryPoint "webs" doesn't exist`}, Err: []string{`entryPoint "webs" doesn't exist`},
Using: []string{"web"},
}, },
}, },
}, },
@ -169,6 +171,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) {
Rule: "Host(`bar.foo`)", Rule: "Host(`bar.foo`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web"},
}, },
"foobar": { "foobar": {
Router: &dynamic.Router{ Router: &dynamic.Router{
@ -177,6 +180,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) {
Rule: "Host(`bar.foobar`)", Rule: "Host(`bar.foobar`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web", "webs"},
}, },
}, },
"webs": { "webs": {
@ -188,6 +192,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) {
Rule: "Host(`foo.bar`)", Rule: "Host(`foo.bar`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"webs"},
}, },
"foobar": { "foobar": {
Router: &dynamic.Router{ Router: &dynamic.Router{
@ -196,6 +201,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) {
Rule: "Host(`bar.foobar`)", Rule: "Host(`bar.foobar`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web", "webs"},
}, },
}, },
}, },

View file

@ -2,24 +2,30 @@ package runtime
import ( import (
"context" "context"
"fmt"
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
) )
// GetTCPRoutersByEntryPoints returns all the tcp routers by entry points name and routers name // GetTCPRoutersByEntryPoints returns all the tcp routers by entry points name and routers name.
func (c *Configuration) GetTCPRoutersByEntryPoints(ctx context.Context, entryPoints []string) map[string]map[string]*TCPRouterInfo { func (c *Configuration) GetTCPRoutersByEntryPoints(ctx context.Context, entryPoints []string) map[string]map[string]*TCPRouterInfo {
entryPointsRouters := make(map[string]map[string]*TCPRouterInfo) entryPointsRouters := make(map[string]map[string]*TCPRouterInfo)
for rtName, rt := range c.TCPRouters { for rtName, rt := range c.TCPRouters {
logger := log.FromContext(log.With(ctx, log.Str(log.RouterName, rtName)))
eps := rt.EntryPoints eps := rt.EntryPoints
if len(eps) == 0 { if len(eps) == 0 {
logger.Debugf("No entryPoint defined for this router, using the default one(s) instead: %+v", entryPoints)
eps = entryPoints eps = entryPoints
} }
entryPointsCount := 0
for _, entryPointName := range eps { for _, entryPointName := range eps {
if !contains(entryPoints, entryPointName) { if !contains(entryPoints, entryPointName) {
log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))). rt.AddError(fmt.Errorf("entryPoint %q doesn't exist", entryPointName), false)
logger.WithField(log.EntryPointName, entryPointName).
Errorf("entryPoint %q doesn't exist", entryPointName) Errorf("entryPoint %q doesn't exist", entryPointName)
continue continue
} }
@ -28,21 +34,30 @@ func (c *Configuration) GetTCPRoutersByEntryPoints(ctx context.Context, entryPoi
entryPointsRouters[entryPointName] = make(map[string]*TCPRouterInfo) entryPointsRouters[entryPointName] = make(map[string]*TCPRouterInfo)
} }
entryPointsCount++
rt.Using = append(rt.Using, entryPointName)
entryPointsRouters[entryPointName][rtName] = rt entryPointsRouters[entryPointName][rtName] = rt
} }
if entryPointsCount == 0 {
rt.AddError(fmt.Errorf("no valid entryPoint for this router"), true)
logger.Error("no valid entryPoint for this router")
}
} }
return entryPointsRouters return entryPointsRouters
} }
// TCPRouterInfo holds information about a currently running TCP router // TCPRouterInfo holds information about a currently running TCP router.
type TCPRouterInfo struct { type TCPRouterInfo struct {
*dynamic.TCPRouter // dynamic configuration *dynamic.TCPRouter // dynamic configuration
Err []string `json:"error,omitempty"` // initialization error Err []string `json:"error,omitempty"` // initialization error
// Status reports whether the router is disabled, in a warning state, or all good (enabled). // Status reports whether the router is disabled, in a warning state, or all good (enabled).
// If not in "enabled" state, the reason for it should be in the list of Err. // If not in "enabled" state, the reason for it should be in the list of Err.
// It is the caller's responsibility to set the initial status. // It is the caller's responsibility to set the initial status.
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Using []string `json:"using,omitempty"` // Effective entry points used by that router.
} }
// AddError adds err to r.Err, if it does not already exist. // AddError adds err to r.Err, if it does not already exist.
@ -66,7 +81,7 @@ func (r *TCPRouterInfo) AddError(err error, critical bool) {
} }
} }
// TCPServiceInfo holds information about a currently running TCP service // TCPServiceInfo holds information about a currently running TCP service.
type TCPServiceInfo struct { type TCPServiceInfo struct {
*dynamic.TCPService // dynamic configuration *dynamic.TCPService // dynamic configuration
Err []string `json:"error,omitempty"` // initialization error Err []string `json:"error,omitempty"` // initialization error

View file

@ -104,6 +104,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) {
Rule: "HostSNI(`bar.foo`)", Rule: "HostSNI(`bar.foo`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web"},
}, },
"foobar": { "foobar": {
TCPRouter: &dynamic.TCPRouter{ TCPRouter: &dynamic.TCPRouter{
@ -111,7 +112,9 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) {
Service: "foobar-service@myprovider", Service: "foobar-service@myprovider",
Rule: "HostSNI(`bar.foobar`)", Rule: "HostSNI(`bar.foobar`)",
}, },
Status: "enabled", Status: "warning",
Err: []string{`entryPoint "webs" doesn't exist`},
Using: []string{"web"},
}, },
}, },
}, },
@ -168,6 +171,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) {
Rule: "HostSNI(`bar.foo`)", Rule: "HostSNI(`bar.foo`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web"},
}, },
"foobar": { "foobar": {
TCPRouter: &dynamic.TCPRouter{ TCPRouter: &dynamic.TCPRouter{
@ -176,6 +180,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) {
Rule: "HostSNI(`bar.foobar`)", Rule: "HostSNI(`bar.foobar`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web", "webs"},
}, },
}, },
"webs": { "webs": {
@ -187,6 +192,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) {
Rule: "HostSNI(`foo.bar`)", Rule: "HostSNI(`foo.bar`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"webs"},
}, },
"foobar": { "foobar": {
TCPRouter: &dynamic.TCPRouter{ TCPRouter: &dynamic.TCPRouter{
@ -195,6 +201,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) {
Rule: "HostSNI(`bar.foobar`)", Rule: "HostSNI(`bar.foobar`)",
}, },
Status: "enabled", Status: "enabled",
Using: []string{"web", "webs"},
}, },
}, },
}, },

View file

@ -144,7 +144,6 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
} }
conf.Routers[serviceName].TLS = tlsConf conf.Routers[serviceName].TLS = tlsConf
} }
} }
} }