diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index c26803e36..696e34dd2 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -10,7 +10,10 @@ "tls": { "options": "default/mytlsoption" }, - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] }, "default/test2.route-23c7f4c450289ee29016@kubernetescrd": { "entryPoints": [ @@ -21,7 +24,10 @@ ], "service": "default/test2.route-23c7f4c450289ee29016", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/tobestripped`)", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] } }, "middlewares": { @@ -42,10 +48,10 @@ "loadBalancer": { "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 @@ -55,18 +61,18 @@ "default/test.route-6b204d94623b3df4370c@kubernetescrd" ], "serverStatus": { - "http://10.42.0.4:80": "UP", - "http://10.42.0.5:80": "UP" + "http://10.42.0.2:80": "UP", + "http://10.42.0.6:80": "UP" } }, "default/test2.route-23c7f4c450289ee29016@kubernetescrd": { "loadBalancer": { "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 @@ -76,8 +82,8 @@ "default/test2.route-23c7f4c450289ee29016@kubernetescrd" ], "serverStatus": { - "http://10.42.0.4:80": "UP", - "http://10.42.0.5:80": "UP" + "http://10.42.0.2:80": "UP", + "http://10.42.0.6:80": "UP" } } }, @@ -92,7 +98,10 @@ "passthrough": false, "options": "default/mytlsoption" }, - "status": "enabled" + "status": "enabled", + "using": [ + "footcp" + ] } }, "tcpServices": { @@ -100,10 +109,10 @@ "loadBalancer": { "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" } ] }, diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 9a9e1b9d7..4908d6388 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -4,17 +4,29 @@ "service": "default/whoami/http", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", "tls": {}, - "status": "enabled" + "status": "enabled", + "using": [ + "traefik", + "web" + ] }, "whoami-test-https/whoami@kubernetes": { "service": "default/whoami/http", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", - "status": "enabled" + "status": "enabled", + "using": [ + "traefik", + "web" + ] }, "whoami-test/whoami@kubernetes": { "service": "default/whoami/http", "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", - "status": "enabled" + "status": "enabled", + "using": [ + "traefik", + "web" + ] } }, "services": { @@ -22,10 +34,10 @@ "loadBalancer": { "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 @@ -37,8 +49,8 @@ "whoami-test/whoami@kubernetes" ], "serverStatus": { - "http://10.42.0.4:80": "UP", - "http://10.42.0.5:80": "UP" + "http://10.42.0.2:80": "UP", + "http://10.42.0.4:80": "UP" } } } diff --git a/pkg/api/criterion.go b/pkg/api/criterion.go new file mode 100644 index 000000000..d81a9f717 --- /dev/null +++ b/pkg/api/criterion.go @@ -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 +} diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 7ee900c75..4b707c568 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -2,9 +2,8 @@ package api import ( "encoding/json" - "fmt" "net/http" - "strconv" + "reflect" "strings" "github.com/containous/traefik/v2/pkg/config/runtime" @@ -15,12 +14,19 @@ import ( "github.com/gorilla/mux" ) -const ( - defaultPerPage = 100 - defaultPage = 1 -) +type apiError struct { + Message string `json:"message"` +} -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 { *runtime.ServiceInfo @@ -36,12 +42,6 @@ type RunTimeRepresentation struct { 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. type Handler struct { 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 { 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 "" +} diff --git a/pkg/api/handler_entrypoint.go b/pkg/api/handler_entrypoint.go index b4cc3f4e5..808b7e536 100644 --- a/pkg/api/handler_entrypoint.go +++ b/pkg/api/handler_entrypoint.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "net/http" "sort" "strconv" @@ -30,28 +31,31 @@ func (h Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) { return results[i].Name < results[j].Name }) + rw.Header().Set("Content-Type", "application/json") + pageInfo, err := pagination(request, len(results)) if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + writeError(rw, err.Error(), http.StatusBadRequest) return } - rw.Header().Set("Content-Type", "application/json") rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) if err != nil { 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) { entryPointID := mux.Vars(request)["entryPointID"] + rw.Header().Set("Content-Type", "application/json") + ep, ok := h.staticConfig.EntryPoints[entryPointID] if !ok { - http.NotFound(rw, request) + writeError(rw, fmt.Sprintf("entry point not found: %s", entryPointID), http.StatusNotFound) return } @@ -60,11 +64,9 @@ func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) { Name: entryPointID, } - rw.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) + writeError(rw, err.Error(), http.StatusInternalServerError) } } diff --git a/pkg/api/handler_http.go b/pkg/api/handler_http.go index 39bc3fd3f..fe43c215f 100644 --- a/pkg/api/handler_http.go +++ b/pkg/api/handler_http.go @@ -2,9 +2,11 @@ package api import ( "encoding/json" + "fmt" "net/http" "sort" "strconv" + "strings" "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/log" @@ -17,182 +19,224 @@ type routerRepresentation struct { 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 { *runtime.ServiceInfo ServerStatus map[string]string `json:"serverStatus,omitempty"` Name string `json:"name,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 { *runtime.MiddlewareInfo Name string `json:"name,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) { results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers)) + criterion := newSearchCriterion(request.URL.Query()) + for name, rt := range h.runtimeConfiguration.Routers { - results = append(results, routerRepresentation{ - RouterInfo: rt, - Name: name, - Provider: getProviderName(name), - }) + if keepRouter(name, rt, criterion) { + results = append(results, newRouterRepresentation(name, rt)) + } } sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) + rw.Header().Set("Content-Type", "application/json") + pageInfo, err := pagination(request, len(results)) if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + writeError(rw, err.Error(), http.StatusBadRequest) return } - rw.Header().Set("Content-Type", "application/json") rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) if err != nil { 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) { routerID := mux.Vars(request)["routerID"] + rw.Header().Set("Content-Type", "application/json") + router, ok := h.runtimeConfiguration.Routers[routerID] if !ok { - http.NotFound(rw, request) + writeError(rw, fmt.Sprintf("router not found: %s", routerID), http.StatusNotFound) return } - result := routerRepresentation{ - RouterInfo: router, - Name: routerID, - Provider: getProviderName(routerID), - } - - rw.Header().Set("Content-Type", "application/json") + result := newRouterRepresentation(routerID, router) err := json.NewEncoder(rw).Encode(result) if err != nil { 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) { results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services)) + criterion := newSearchCriterion(request.URL.Query()) + for name, si := range h.runtimeConfiguration.Services { - results = append(results, serviceRepresentation{ - ServiceInfo: si, - Name: name, - Provider: getProviderName(name), - ServerStatus: si.GetAllStatus(), - }) + if keepService(name, si, criterion) { + results = append(results, newServiceRepresentation(name, si)) + } } sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) + rw.Header().Set("Content-Type", "application/json") + pageInfo, err := pagination(request, len(results)) if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + writeError(rw, err.Error(), http.StatusBadRequest) return } - rw.Header().Set("Content-Type", "application/json") rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) if err != nil { 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) { serviceID := mux.Vars(request)["serviceID"] + rw.Header().Add("Content-Type", "application/json") + service, ok := h.runtimeConfiguration.Services[serviceID] if !ok { - http.NotFound(rw, request) + writeError(rw, fmt.Sprintf("service not found: %s", serviceID), http.StatusNotFound) return } - result := serviceRepresentation{ - ServiceInfo: service, - Name: serviceID, - Provider: getProviderName(serviceID), - ServerStatus: service.GetAllStatus(), - } - - rw.Header().Add("Content-Type", "application/json") + result := newServiceRepresentation(serviceID, service) err := json.NewEncoder(rw).Encode(result) if err != nil { 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) { results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares)) + criterion := newSearchCriterion(request.URL.Query()) + for name, mi := range h.runtimeConfiguration.Middlewares { - results = append(results, middlewareRepresentation{ - MiddlewareInfo: mi, - Name: name, - Provider: getProviderName(name), - }) + if keepMiddleware(name, mi, criterion) { + results = append(results, newMiddlewareRepresentation(name, mi)) + } } sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) + rw.Header().Set("Content-Type", "application/json") + pageInfo, err := pagination(request, len(results)) if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + writeError(rw, err.Error(), http.StatusBadRequest) return } - rw.Header().Set("Content-Type", "application/json") rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) if err != nil { 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) { middlewareID := mux.Vars(request)["middlewareID"] + rw.Header().Set("Content-Type", "application/json") + middleware, ok := h.runtimeConfiguration.Middlewares[middlewareID] if !ok { - http.NotFound(rw, request) + writeError(rw, fmt.Sprintf("middleware not found: %s", middlewareID), http.StatusNotFound) return } - result := middlewareRepresentation{ - MiddlewareInfo: middleware, - Name: middlewareID, - Provider: getProviderName(middlewareID), - } - - rw.Header().Set("Content-Type", "application/json") + result := newMiddlewareRepresentation(middlewareID, middleware) err := json.NewEncoder(rw).Encode(result) if err != nil { 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) +} diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go index 57524e675..38286151f 100644 --- a/pkg/api/handler_http_test.go +++ b/pkg/api/handler_http_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -137,6 +138,68 @@ func TestHandler_HTTP(t *testing.T) { 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", path: "/api/http/routers/bar@myprovider", @@ -232,6 +295,45 @@ func TestHandler_HTTP(t *testing.T) { si.UpdateServerStatus("http://127.0.0.2", "UP") 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{ @@ -301,6 +403,100 @@ func TestHandler_HTTP(t *testing.T) { 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", path: "/api/http/services/bar@myprovider", @@ -411,6 +607,86 @@ func TestHandler_HTTP(t *testing.T) { 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", path: "/api/http/middlewares?page=2&per_page=1", @@ -521,6 +797,8 @@ func TestHandler_HTTP(t *testing.T) { rtConf := &test.conf // To lazily initialize the Statuses. rtConf.PopulateUsedBy() + rtConf.GetRoutersByEntryPoints(context.Background(), []string{"web"}, false) + handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) router := mux.NewRouter() handler.Append(router) diff --git a/pkg/api/handler_overview.go b/pkg/api/handler_overview.go index f08d80351..21f1921e1 100644 --- a/pkg/api/handler_overview.go +++ b/pkg/api/handler_overview.go @@ -56,7 +56,7 @@ func (h Handler) getOverview(rw http.ResponseWriter, request *http.Request) { err := json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) - http.Error(rw, err.Error(), http.StatusInternalServerError) + writeError(rw, err.Error(), http.StatusInternalServerError) } } diff --git a/pkg/api/handler_tcp.go b/pkg/api/handler_tcp.go index 3f91f0386..823af5178 100644 --- a/pkg/api/handler_tcp.go +++ b/pkg/api/handler_tcp.go @@ -2,9 +2,11 @@ package api import ( "encoding/json" + "fmt" "net/http" "sort" "strconv" + "strings" "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/log" @@ -17,118 +19,146 @@ type tcpRouterRepresentation struct { 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 { *runtime.TCPServiceInfo Name string `json:"name,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) { results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters)) + criterion := newSearchCriterion(request.URL.Query()) + for name, rt := range h.runtimeConfiguration.TCPRouters { - results = append(results, tcpRouterRepresentation{ - TCPRouterInfo: rt, - Name: name, - Provider: getProviderName(name), - }) + if keepTCPRouter(name, rt, criterion) { + results = append(results, newTCPRouterRepresentation(name, rt)) + } } sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) + rw.Header().Set("Content-Type", "application/json") + pageInfo, err := pagination(request, len(results)) if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + writeError(rw, err.Error(), http.StatusBadRequest) return } - rw.Header().Set("Content-Type", "application/json") rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) if err != nil { 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) { routerID := mux.Vars(request)["routerID"] + rw.Header().Set("Content-Type", "application/json") + router, ok := h.runtimeConfiguration.TCPRouters[routerID] if !ok { - http.NotFound(rw, request) + writeError(rw, fmt.Sprintf("router not found: %s", routerID), http.StatusNotFound) return } - result := tcpRouterRepresentation{ - TCPRouterInfo: router, - Name: routerID, - Provider: getProviderName(routerID), - } - - rw.Header().Set("Content-Type", "application/json") + result := newTCPRouterRepresentation(routerID, router) err := json.NewEncoder(rw).Encode(result) if err != nil { 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) { results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices)) + criterion := newSearchCriterion(request.URL.Query()) + for name, si := range h.runtimeConfiguration.TCPServices { - results = append(results, tcpServiceRepresentation{ - TCPServiceInfo: si, - Name: name, - Provider: getProviderName(name), - }) + if keepTCPService(name, si, criterion) { + results = append(results, newTCPServiceRepresentation(name, si)) + } } sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name }) + rw.Header().Set("Content-Type", "application/json") + pageInfo, err := pagination(request, len(results)) if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + writeError(rw, err.Error(), http.StatusBadRequest) return } - rw.Header().Set("Content-Type", "application/json") rw.Header().Set(nextPageHeader, strconv.Itoa(pageInfo.nextPage)) err = json.NewEncoder(rw).Encode(results[pageInfo.startIndex:pageInfo.endIndex]) if err != nil { 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) { serviceID := mux.Vars(request)["serviceID"] + rw.Header().Set("Content-Type", "application/json") + service, ok := h.runtimeConfiguration.TCPServices[serviceID] if !ok { - http.NotFound(rw, request) + writeError(rw, fmt.Sprintf("service not found: %s", serviceID), http.StatusNotFound) return } - result := tcpServiceRepresentation{ - TCPServiceInfo: service, - Name: serviceID, - Provider: getProviderName(serviceID), - } - - rw.Header().Set("Content-Type", "application/json") + result := newTCPServiceRepresentation(serviceID, service) err := json.NewEncoder(rw).Encode(result) if err != nil { 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) +} diff --git a/pkg/api/handler_tcp_test.go b/pkg/api/handler_tcp_test.go index 28c833f8b..798876de4 100644 --- a/pkg/api/handler_tcp_test.go +++ b/pkg/api/handler_tcp_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "io/ioutil" "net/http" @@ -112,6 +113,86 @@ func TestHandler_TCP(t *testing.T) { 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", path: "/api/tcp/routers/bar@myprovider", @@ -219,6 +300,110 @@ func TestHandler_TCP(t *testing.T) { 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", path: "/api/tcp/services?page=2&per_page=1", @@ -330,6 +515,10 @@ func TestHandler_TCP(t *testing.T) { t.Parallel() 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) router := mux.NewRouter() handler.Append(router) diff --git a/pkg/api/testdata/middleware-auth.json b/pkg/api/testdata/middleware-auth.json index 0ed588873..3c7ff53fa 100644 --- a/pkg/api/testdata/middleware-auth.json +++ b/pkg/api/testdata/middleware-auth.json @@ -7,6 +7,7 @@ "name": "auth@myprovider", "provider": "myprovider", "status": "enabled", + "type": "basicauth", "usedBy": [ "bar@myprovider", "test@myprovider" diff --git a/pkg/api/testdata/middlewares-filtered-search.json b/pkg/api/testdata/middlewares-filtered-search.json new file mode 100644 index 000000000..ba1382268 --- /dev/null +++ b/pkg/api/testdata/middlewares-filtered-search.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/middlewares-filtered-status.json b/pkg/api/testdata/middlewares-filtered-status.json new file mode 100644 index 000000000..eab60c6a4 --- /dev/null +++ b/pkg/api/testdata/middlewares-filtered-status.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/middlewares-page2.json b/pkg/api/testdata/middlewares-page2.json index 25ebc13f5..ecaf5978a 100644 --- a/pkg/api/testdata/middlewares-page2.json +++ b/pkg/api/testdata/middlewares-page2.json @@ -6,6 +6,7 @@ "name": "addPrefixTest@myprovider", "provider": "myprovider", "status": "enabled", + "type": "addprefix", "usedBy": [ "test@myprovider" ] diff --git a/pkg/api/testdata/middlewares.json b/pkg/api/testdata/middlewares.json index c27533ee1..f48f52efb 100644 --- a/pkg/api/testdata/middlewares.json +++ b/pkg/api/testdata/middlewares.json @@ -6,6 +6,7 @@ "name": "addPrefixTest@anotherprovider", "provider": "anotherprovider", "status": "enabled", + "type": "addprefix", "usedBy": [ "bar@myprovider" ] @@ -17,6 +18,7 @@ "name": "addPrefixTest@myprovider", "provider": "myprovider", "status": "enabled", + "type": "addprefix", "usedBy": [ "test@myprovider" ] @@ -30,6 +32,7 @@ "name": "auth@myprovider", "provider": "myprovider", "status": "enabled", + "type": "basicauth", "usedBy": [ "bar@myprovider", "test@myprovider" diff --git a/pkg/api/testdata/router-bar.json b/pkg/api/testdata/router-bar.json index 267a9628b..098ea0b1a 100644 --- a/pkg/api/testdata/router-bar.json +++ b/pkg/api/testdata/router-bar.json @@ -10,5 +10,8 @@ "provider": "myprovider", "rule": "Host(`foo.bar`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] } \ No newline at end of file diff --git a/pkg/api/testdata/routers-filtered-search.json b/pkg/api/testdata/routers-filtered-search.json new file mode 100644 index 000000000..4e251f5da --- /dev/null +++ b/pkg/api/testdata/routers-filtered-search.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/routers-filtered-status.json b/pkg/api/testdata/routers-filtered-status.json new file mode 100644 index 000000000..1c96e3802 --- /dev/null +++ b/pkg/api/testdata/routers-filtered-status.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/routers-many-lastpage.json b/pkg/api/testdata/routers-many-lastpage.json index 290672375..df1ee7929 100644 --- a/pkg/api/testdata/routers-many-lastpage.json +++ b/pkg/api/testdata/routers-many-lastpage.json @@ -7,7 +7,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar14`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -17,7 +20,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar15`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -27,7 +33,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar16`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -37,7 +46,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar17`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -47,6 +59,9 @@ "provider": "myprovider", "rule": "Host(`foo.bar18`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] } ] \ No newline at end of file diff --git a/pkg/api/testdata/routers-page2.json b/pkg/api/testdata/routers-page2.json index 8c7f65d1b..579e2a04f 100644 --- a/pkg/api/testdata/routers-page2.json +++ b/pkg/api/testdata/routers-page2.json @@ -7,6 +7,9 @@ "provider": "myprovider", "rule": "Host(`toto.bar`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] } ] \ No newline at end of file diff --git a/pkg/api/testdata/routers.json b/pkg/api/testdata/routers.json index 664aedc51..ed0d10e28 100644 --- a/pkg/api/testdata/routers.json +++ b/pkg/api/testdata/routers.json @@ -11,7 +11,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -25,6 +28,9 @@ "provider": "myprovider", "rule": "Host(`foo.bar.other`)", "service": "foo-service@myprovider", - "status": "enabled" + "status": "enabled", + "using": [ + "web" + ] } ] \ No newline at end of file diff --git a/pkg/api/testdata/service-bar.json b/pkg/api/testdata/service-bar.json index e26b65a72..11e832136 100644 --- a/pkg/api/testdata/service-bar.json +++ b/pkg/api/testdata/service-bar.json @@ -13,6 +13,7 @@ "http://127.0.0.1": "UP" }, "status": "enabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider", "test@myprovider" diff --git a/pkg/api/testdata/services-filtered-search.json b/pkg/api/testdata/services-filtered-search.json new file mode 100644 index 000000000..fd854ed72 --- /dev/null +++ b/pkg/api/testdata/services-filtered-search.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/services-filtered-status.json b/pkg/api/testdata/services-filtered-status.json new file mode 100644 index 000000000..df1a52dcb --- /dev/null +++ b/pkg/api/testdata/services-filtered-status.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/services-page2.json b/pkg/api/testdata/services-page2.json index 66b8390d1..9b8b5ce72 100644 --- a/pkg/api/testdata/services-page2.json +++ b/pkg/api/testdata/services-page2.json @@ -14,6 +14,7 @@ "http://127.0.0.2": "UP" }, "status": "enabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider" ] diff --git a/pkg/api/testdata/services.json b/pkg/api/testdata/services.json index bd54b536c..e96abc23b 100644 --- a/pkg/api/testdata/services.json +++ b/pkg/api/testdata/services.json @@ -14,6 +14,7 @@ "http://127.0.0.1": "UP" }, "status": "enabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider", "test@myprovider" @@ -34,6 +35,51 @@ "http://127.0.0.2": "UP" }, "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": [ "foo@myprovider" ] diff --git a/pkg/api/testdata/tcprouter-bar.json b/pkg/api/testdata/tcprouter-bar.json index b6b244199..70d06a3ea 100644 --- a/pkg/api/testdata/tcprouter-bar.json +++ b/pkg/api/testdata/tcprouter-bar.json @@ -5,5 +5,9 @@ "name": "bar@myprovider", "provider": "myprovider", "rule": "Host(`foo.bar`)", - "service": "foo-service@myprovider" + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] } \ No newline at end of file diff --git a/pkg/api/testdata/tcprouters-filtered-search.json b/pkg/api/testdata/tcprouters-filtered-search.json new file mode 100644 index 000000000..4593f1b03 --- /dev/null +++ b/pkg/api/testdata/tcprouters-filtered-search.json @@ -0,0 +1,15 @@ +[ + { + "entryPoints": [ + "web" + ], + "name": "bar@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "foo-service@myprovider", + "status": "warning", + "using": [ + "web" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/tcprouters-filtered-status.json b/pkg/api/testdata/tcprouters-filtered-status.json new file mode 100644 index 000000000..64d232ebc --- /dev/null +++ b/pkg/api/testdata/tcprouters-filtered-status.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/tcprouters-page2.json b/pkg/api/testdata/tcprouters-page2.json index 73d97c6d6..579e2a04f 100644 --- a/pkg/api/testdata/tcprouters-page2.json +++ b/pkg/api/testdata/tcprouters-page2.json @@ -6,6 +6,10 @@ "name": "baz@myprovider", "provider": "myprovider", "rule": "Host(`toto.bar`)", - "service": "foo-service@myprovider" + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] } ] \ No newline at end of file diff --git a/pkg/api/testdata/tcprouters.json b/pkg/api/testdata/tcprouters.json index a2bf3c401..bf6bffc17 100644 --- a/pkg/api/testdata/tcprouters.json +++ b/pkg/api/testdata/tcprouters.json @@ -7,7 +7,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar`)", "service": "foo-service@myprovider", - "status": "warning" + "status": "warning", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -17,7 +20,10 @@ "provider": "myprovider", "rule": "Host(`foo.bar`)", "service": "foo-service@myprovider", - "status": "disabled" + "status": "disabled", + "using": [ + "web" + ] }, { "entryPoints": [ @@ -30,6 +36,9 @@ "status": "enabled", "tls": { "passthrough": false - } + }, + "using": [ + "web" + ] } ] \ No newline at end of file diff --git a/pkg/api/testdata/tcpservice-bar.json b/pkg/api/testdata/tcpservice-bar.json index 114f0b74b..ade480a92 100644 --- a/pkg/api/testdata/tcpservice-bar.json +++ b/pkg/api/testdata/tcpservice-bar.json @@ -8,6 +8,8 @@ }, "name": "bar@myprovider", "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider", "test@myprovider" diff --git a/pkg/api/testdata/tcpservices-filtered-search.json b/pkg/api/testdata/tcpservices-filtered-search.json new file mode 100644 index 000000000..130d5eace --- /dev/null +++ b/pkg/api/testdata/tcpservices-filtered-search.json @@ -0,0 +1,18 @@ +[ + { + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.2:2345" + } + ] + }, + "name": "baz@myprovider", + "provider": "myprovider", + "status": "warning", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/tcpservices-filtered-status.json b/pkg/api/testdata/tcpservices-filtered-status.json new file mode 100644 index 000000000..03ec085a0 --- /dev/null +++ b/pkg/api/testdata/tcpservices-filtered-status.json @@ -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" + ] + } +] \ No newline at end of file diff --git a/pkg/api/testdata/tcpservices-page2.json b/pkg/api/testdata/tcpservices-page2.json index 345151040..414e0f37d 100644 --- a/pkg/api/testdata/tcpservices-page2.json +++ b/pkg/api/testdata/tcpservices-page2.json @@ -9,6 +9,8 @@ }, "name": "baz@myprovider", "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider" ] diff --git a/pkg/api/testdata/tcpservices.json b/pkg/api/testdata/tcpservices.json index 24320d948..c3f9f7ea6 100644 --- a/pkg/api/testdata/tcpservices.json +++ b/pkg/api/testdata/tcpservices.json @@ -10,6 +10,7 @@ "name": "bar@myprovider", "provider": "myprovider", "status": "enabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider", "test@myprovider" @@ -26,6 +27,7 @@ "name": "baz@myprovider", "provider": "myprovider", "status": "warning", + "type": "loadbalancer", "usedBy": [ "foo@myprovider" ] @@ -41,6 +43,7 @@ "name": "foz@myprovider", "provider": "myprovider", "status": "disabled", + "type": "loadbalancer", "usedBy": [ "foo@myprovider" ] diff --git a/pkg/config/runtime/runtime_http.go b/pkg/config/runtime/runtime_http.go index e604ac32c..af5062fd9 100644 --- a/pkg/config/runtime/runtime_http.go +++ b/pkg/config/runtime/runtime_http.go @@ -3,13 +3,14 @@ package runtime import ( "context" "fmt" + "sort" "sync" "github.com/containous/traefik/v2/pkg/config/dynamic" "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 { 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))) + eps := rt.EntryPoints 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 } + entryPointsCount := 0 for _, entryPointName := range eps { if !contains(entryPoints, entryPointName) { @@ -33,23 +36,44 @@ func (c *Configuration) GetRoutersByEntryPoints(ctx context.Context, entryPoints continue } - entryPointsCount++ if _, ok := entryPointsRouters[entryPointName]; !ok { entryPointsRouters[entryPointName] = make(map[string]*RouterInfo) } + entryPointsCount++ + rt.Using = append(rt.Using, entryPointName) + 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") } + + rt.Using = unique(rt.Using) } 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 { *dynamic.Router // dynamic configuration // 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). // 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. - 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. @@ -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 { *dynamic.Middleware // dynamic configuration // Err contains all the errors that occurred during service creation. Err []string `json:"error,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. @@ -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 { *dynamic.Service // dynamic configuration // Err contains all the errors that occurred during service creation. diff --git a/pkg/config/runtime/runtime_http_test.go b/pkg/config/runtime/runtime_http_test.go index d95c6e780..909d664b7 100644 --- a/pkg/config/runtime/runtime_http_test.go +++ b/pkg/config/runtime/runtime_http_test.go @@ -104,6 +104,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) { Rule: "Host(`bar.foo`)", }, Status: "enabled", + Using: []string{"web"}, }, "foobar": { Router: &dynamic.Router{ @@ -113,6 +114,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) { }, Status: "warning", Err: []string{`entryPoint "webs" doesn't exist`}, + Using: []string{"web"}, }, }, }, @@ -169,6 +171,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) { Rule: "Host(`bar.foo`)", }, Status: "enabled", + Using: []string{"web"}, }, "foobar": { Router: &dynamic.Router{ @@ -177,6 +180,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) { Rule: "Host(`bar.foobar`)", }, Status: "enabled", + Using: []string{"web", "webs"}, }, }, "webs": { @@ -188,6 +192,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) { Rule: "Host(`foo.bar`)", }, Status: "enabled", + Using: []string{"webs"}, }, "foobar": { Router: &dynamic.Router{ @@ -196,6 +201,7 @@ func TestGetRoutersByEntryPoints(t *testing.T) { Rule: "Host(`bar.foobar`)", }, Status: "enabled", + Using: []string{"web", "webs"}, }, }, }, diff --git a/pkg/config/runtime/runtime_tcp.go b/pkg/config/runtime/runtime_tcp.go index a99d519f9..427412bdb 100644 --- a/pkg/config/runtime/runtime_tcp.go +++ b/pkg/config/runtime/runtime_tcp.go @@ -2,24 +2,30 @@ package runtime import ( "context" + "fmt" "github.com/containous/traefik/v2/pkg/config/dynamic" "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 { entryPointsRouters := make(map[string]map[string]*TCPRouterInfo) for rtName, rt := range c.TCPRouters { + logger := log.FromContext(log.With(ctx, log.Str(log.RouterName, rtName))) + eps := rt.EntryPoints if len(eps) == 0 { + logger.Debugf("No entryPoint defined for this router, using the default one(s) instead: %+v", entryPoints) eps = entryPoints } + entryPointsCount := 0 for _, entryPointName := range eps { 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) continue } @@ -28,21 +34,30 @@ func (c *Configuration) GetTCPRoutersByEntryPoints(ctx context.Context, entryPoi entryPointsRouters[entryPointName] = make(map[string]*TCPRouterInfo) } + entryPointsCount++ + rt.Using = append(rt.Using, entryPointName) + 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 } -// TCPRouterInfo holds information about a currently running TCP router +// TCPRouterInfo holds information about a currently running TCP router. type TCPRouterInfo struct { *dynamic.TCPRouter // dynamic configuration Err []string `json:"error,omitempty"` // initialization error // 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. // 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. @@ -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 { *dynamic.TCPService // dynamic configuration Err []string `json:"error,omitempty"` // initialization error diff --git a/pkg/config/runtime/runtime_tcp_test.go b/pkg/config/runtime/runtime_tcp_test.go index 9fb3f137e..8df42fc9b 100644 --- a/pkg/config/runtime/runtime_tcp_test.go +++ b/pkg/config/runtime/runtime_tcp_test.go @@ -104,6 +104,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) { Rule: "HostSNI(`bar.foo`)", }, Status: "enabled", + Using: []string{"web"}, }, "foobar": { TCPRouter: &dynamic.TCPRouter{ @@ -111,7 +112,9 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) { Service: "foobar-service@myprovider", 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`)", }, Status: "enabled", + Using: []string{"web"}, }, "foobar": { TCPRouter: &dynamic.TCPRouter{ @@ -176,6 +180,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) { Rule: "HostSNI(`bar.foobar`)", }, Status: "enabled", + Using: []string{"web", "webs"}, }, }, "webs": { @@ -187,6 +192,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) { Rule: "HostSNI(`foo.bar`)", }, Status: "enabled", + Using: []string{"webs"}, }, "foobar": { TCPRouter: &dynamic.TCPRouter{ @@ -195,6 +201,7 @@ func TestGetTCPRoutersByEntryPoints(t *testing.T) { Rule: "HostSNI(`bar.foobar`)", }, Status: "enabled", + Using: []string{"web", "webs"}, }, }, }, diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 52d18dd30..059e41b9c 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -144,7 +144,6 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli } conf.Routers[serviceName].TLS = tlsConf } - } }