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": {
"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"
}
]
},

View file

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

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 (
"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 ""
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@
"name": "auth@myprovider",
"provider": "myprovider",
"status": "enabled",
"type": "basicauth",
"usedBy": [
"bar@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",
"provider": "myprovider",
"status": "enabled",
"type": "addprefix",
"usedBy": [
"test@myprovider"
]

View file

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

View file

@ -10,5 +10,8 @@
"provider": "myprovider",
"rule": "Host(`foo.bar`)",
"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",
"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"
]
}
]

View file

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

View file

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

View file

@ -13,6 +13,7 @@
"http://127.0.0.1": "UP"
},
"status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@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"
},
"status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider"
]

View file

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

View file

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

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",
"provider": "myprovider",
"rule": "Host(`toto.bar`)",
"service": "foo-service@myprovider"
"service": "foo-service@myprovider",
"status": "enabled",
"using": [
"web"
]
}
]

View file

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

View file

@ -8,6 +8,8 @@
},
"name": "bar@myprovider",
"provider": "myprovider",
"status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@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",
"provider": "myprovider",
"status": "enabled",
"type": "loadbalancer",
"usedBy": [
"foo@myprovider"
]

View file

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

View file

@ -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.
@ -58,6 +82,7 @@ type RouterInfo struct {
// 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"`
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.

View file

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

View file

@ -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,14 +34,22 @@ 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
@ -43,6 +57,7 @@ type TCPRouterInfo struct {
// 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"`
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

View file

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

View file

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