diff --git a/docs/content/operations/api.md b/docs/content/operations/api.md index a7a4c41f1..70117dc7a 100644 --- a/docs/content/operations/api.md +++ b/docs/content/operations/api.md @@ -111,6 +111,8 @@ All the following endpoints must be accessed with a `GET` HTTP request. | `/api/tcp/routers/{name}` | Returns the information of the TCP router specified by `name`. | | `/api/tcp/services` | Lists all the TCP services information. | | `/api/tcp/services/{name}` | Returns the information of the TCP service specified by `name`. | +| `/api/entrypoints` | Lists all the entry points information. | +| `/api/entrypoints/{name}` | Returns the information of the entry point specified by `name`. | | `/api/version` | Returns information about Traefik version. | | `/debug/vars` | See the [expvar](https://golang.org/pkg/expvar/) Go documentation. | | `/debug/pprof/` | See the [pprof Index](https://golang.org/pkg/net/http/pprof/#Index) Go documentation. | diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 2fecfc9c2..79e0540d7 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "sort" "strconv" "strings" @@ -38,37 +37,6 @@ type RunTimeRepresentation struct { TCPServices map[string]*dynamic.TCPServiceInfo `json:"tcpServices,omitempty"` } -type routerRepresentation struct { - *dynamic.RouterInfo - Name string `json:"name,omitempty"` - Provider string `json:"provider,omitempty"` -} - -type serviceRepresentation struct { - *dynamic.ServiceInfo - ServerStatus map[string]string `json:"serverStatus,omitempty"` - Name string `json:"name,omitempty"` - Provider string `json:"provider,omitempty"` -} - -type middlewareRepresentation struct { - *dynamic.MiddlewareInfo - Name string `json:"name,omitempty"` - Provider string `json:"provider,omitempty"` -} - -type tcpRouterRepresentation struct { - *dynamic.TCPRouterInfo - Name string `json:"name,omitempty"` - Provider string `json:"provider,omitempty"` -} - -type tcpServiceRepresentation struct { - *dynamic.TCPServiceInfo - Name string `json:"name,omitempty"` - Provider string `json:"provider,omitempty"` -} - type pageInfo struct { startIndex int endIndex int @@ -81,6 +49,7 @@ type Handler struct { debug bool // runtimeConfiguration is the data set used to create all the data representations exposed by the API. runtimeConfiguration *dynamic.RuntimeConfiguration + staticConfig static.Configuration statistics *types.Statistics // stats *thoasstats.Stats // FIXME stats // StatsRecorder *middlewares.StatsRecorder // FIXME stats @@ -100,6 +69,7 @@ func New(staticConfig static.Configuration, runtimeConfig *dynamic.RuntimeConfig statistics: staticConfig.API.Statistics, dashboardAssets: staticConfig.API.DashboardAssets, runtimeConfiguration: rConfig, + staticConfig: staticConfig, debug: staticConfig.API.Debug, } } @@ -112,6 +82,12 @@ func (h Handler) Append(router *mux.Router) { router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration) + // Experimental endpoint + router.Methods(http.MethodGet).Path("/api/overview").HandlerFunc(h.getOverview) + + router.Methods(http.MethodGet).Path("/api/entrypoints").HandlerFunc(h.getEntryPoints) + router.Methods(http.MethodGet).Path("/api/entrypoints/{entryPointID}").HandlerFunc(h.getEntryPoint) + router.Methods(http.MethodGet).Path("/api/http/routers").HandlerFunc(h.getRouters) router.Methods(http.MethodGet).Path("/api/http/routers/{routerID}").HandlerFunc(h.getRouter) router.Methods(http.MethodGet).Path("/api/http/services").HandlerFunc(h.getServices) @@ -135,283 +111,6 @@ func (h Handler) Append(router *mux.Router) { } } -func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) { - results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers)) - - for name, rt := range h.runtimeConfiguration.Routers { - results = append(results, routerRepresentation{ - RouterInfo: rt, - Name: name, - Provider: getProviderName(name), - }) - } - - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) - - pageInfo, err := pagination(request, len(results)) - if err != nil { - http.Error(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) - } -} - -func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) { - routerID := mux.Vars(request)["routerID"] - - router, ok := h.runtimeConfiguration.Routers[routerID] - if !ok { - http.NotFound(rw, request) - return - } - - result := routerRepresentation{ - RouterInfo: router, - Name: routerID, - Provider: getProviderName(routerID), - } - - 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) - } -} - -func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) { - results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services)) - - for name, si := range h.runtimeConfiguration.Services { - results = append(results, serviceRepresentation{ - ServiceInfo: si, - Name: name, - Provider: getProviderName(name), - ServerStatus: si.GetAllStatus(), - }) - } - - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) - - pageInfo, err := pagination(request, len(results)) - if err != nil { - http.Error(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) - } -} - -func (h Handler) getService(rw http.ResponseWriter, request *http.Request) { - serviceID := mux.Vars(request)["serviceID"] - - service, ok := h.runtimeConfiguration.Services[serviceID] - if !ok { - http.NotFound(rw, request) - return - } - - result := serviceRepresentation{ - ServiceInfo: service, - Name: serviceID, - Provider: getProviderName(serviceID), - ServerStatus: service.GetAllStatus(), - } - - rw.Header().Add("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) - } -} - -func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) { - results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares)) - - for name, mi := range h.runtimeConfiguration.Middlewares { - results = append(results, middlewareRepresentation{ - MiddlewareInfo: mi, - Name: name, - Provider: getProviderName(name), - }) - } - - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) - - pageInfo, err := pagination(request, len(results)) - if err != nil { - http.Error(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) - } -} - -func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) { - middlewareID := mux.Vars(request)["middlewareID"] - - middleware, ok := h.runtimeConfiguration.Middlewares[middlewareID] - if !ok { - http.NotFound(rw, request) - return - } - - result := middlewareRepresentation{ - MiddlewareInfo: middleware, - Name: middlewareID, - Provider: getProviderName(middlewareID), - } - - 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) - } -} - -func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) { - results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters)) - - for name, rt := range h.runtimeConfiguration.TCPRouters { - results = append(results, tcpRouterRepresentation{ - TCPRouterInfo: rt, - Name: name, - Provider: getProviderName(name), - }) - } - - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) - - pageInfo, err := pagination(request, len(results)) - if err != nil { - http.Error(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) - } -} - -func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) { - routerID := mux.Vars(request)["routerID"] - - router, ok := h.runtimeConfiguration.TCPRouters[routerID] - if !ok { - http.NotFound(rw, request) - return - } - - result := tcpRouterRepresentation{ - TCPRouterInfo: router, - Name: routerID, - Provider: getProviderName(routerID), - } - - 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) - } -} - -func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) { - results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices)) - - for name, si := range h.runtimeConfiguration.TCPServices { - results = append(results, tcpServiceRepresentation{ - TCPServiceInfo: si, - Name: name, - Provider: getProviderName(name), - }) - } - - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) - - pageInfo, err := pagination(request, len(results)) - if err != nil { - http.Error(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) - } -} - -func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) { - serviceID := mux.Vars(request)["serviceID"] - - service, ok := h.runtimeConfiguration.TCPServices[serviceID] - if !ok { - http.NotFound(rw, request) - return - } - - result := tcpServiceRepresentation{ - TCPServiceInfo: service, - Name: serviceID, - Provider: getProviderName(serviceID), - } - - 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) - } -} - func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) { siRepr := make(map[string]*serviceInfoRepresentation, len(h.runtimeConfiguration.Services)) for k, v := range h.runtimeConfiguration.Services { diff --git a/pkg/api/handler_entrypoint.go b/pkg/api/handler_entrypoint.go new file mode 100644 index 000000000..b36dd619c --- /dev/null +++ b/pkg/api/handler_entrypoint.go @@ -0,0 +1,70 @@ +package api + +import ( + "encoding/json" + "net/http" + "sort" + "strconv" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/static" + "github.com/containous/traefik/pkg/log" +) + +type entryPointRepresentation struct { + *static.EntryPoint + Name string `json:"name,omitempty"` +} + +func (h Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) { + results := make([]entryPointRepresentation, 0, len(h.staticConfig.EntryPoints)) + + for name, ep := range h.staticConfig.EntryPoints { + results = append(results, entryPointRepresentation{ + EntryPoint: ep, + Name: name, + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + pageInfo, err := pagination(request, len(results)) + if err != nil { + http.Error(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) + } +} + +func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) { + entryPointID := mux.Vars(request)["entryPointID"] + + ep, ok := h.staticConfig.EntryPoints[entryPointID] + if !ok { + http.NotFound(rw, request) + return + } + + result := entryPointRepresentation{ + EntryPoint: ep, + 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) + } +} diff --git a/pkg/api/handler_entrypoint_test.go b/pkg/api/handler_entrypoint_test.go new file mode 100644 index 000000000..162e655a2 --- /dev/null +++ b/pkg/api/handler_entrypoint_test.go @@ -0,0 +1,253 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/config/static" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandler_EntryPoints(t *testing.T) { + type expected struct { + statusCode int + nextPage string + jsonFile string + } + + testCases := []struct { + desc string + path string + conf static.Configuration + expected expected + }{ + { + desc: "all entry points, but no config", + path: "/api/entrypoints", + conf: static.Configuration{API: &static.API{}, Global: &static.Global{}}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/entrypoints-empty.json", + }, + }, + { + desc: "all entry points", + path: "/api/entrypoints", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: map[string]*static.EntryPoint{ + "web": { + Address: ":80", + Transport: &static.EntryPointsTransport{ + LifeCycle: &static.LifeCycle{ + RequestAcceptGraceTimeout: 1, + GraceTimeOut: 2, + }, + RespondingTimeouts: &static.RespondingTimeouts{ + ReadTimeout: 3, + WriteTimeout: 4, + IdleTimeout: 5, + }, + }, + ProxyProtocol: &static.ProxyProtocol{ + Insecure: true, + TrustedIPs: []string{"192.168.1.1", "192.168.1.2"}, + }, + ForwardedHeaders: &static.ForwardedHeaders{ + Insecure: true, + TrustedIPs: []string{"192.168.1.3", "192.168.1.4"}, + }, + }, + "web-secure": { + Address: ":443", + Transport: &static.EntryPointsTransport{ + LifeCycle: &static.LifeCycle{ + RequestAcceptGraceTimeout: 10, + GraceTimeOut: 20, + }, + RespondingTimeouts: &static.RespondingTimeouts{ + ReadTimeout: 30, + WriteTimeout: 40, + IdleTimeout: 50, + }, + }, + ProxyProtocol: &static.ProxyProtocol{ + Insecure: true, + TrustedIPs: []string{"192.168.1.10", "192.168.1.20"}, + }, + ForwardedHeaders: &static.ForwardedHeaders{ + Insecure: true, + TrustedIPs: []string{"192.168.1.30", "192.168.1.40"}, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/entrypoints.json", + }, + }, + { + desc: "all entry points, pagination, 1 res per page, want page 2", + path: "/api/entrypoints?page=2&per_page=1", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: map[string]*static.EntryPoint{ + "web1": {Address: ":81"}, + "web2": {Address: ":82"}, + "web3": {Address: ":83"}, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/entrypoints-page2.json", + }, + }, + { + desc: "all entry points, pagination, 19 results overall, 7 res per page, want page 3", + path: "/api/entrypoints?page=3&per_page=7", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: generateEntryPoints(19), + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/entrypoints-many-lastpage.json", + }, + }, + { + desc: "all entry points, pagination, 5 results overall, 10 res per page, want page 2", + path: "/api/entrypoints?page=2&per_page=10", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: generateEntryPoints(5), + }, + expected: expected{ + statusCode: http.StatusBadRequest, + }, + }, + { + desc: "all entry points, pagination, 10 results overall, 10 res per page, want page 2", + path: "/api/entrypoints?page=2&per_page=10", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: generateEntryPoints(10), + }, + expected: expected{ + statusCode: http.StatusBadRequest, + }, + }, + { + desc: "one entry point by id", + path: "/api/entrypoints/bar", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: map[string]*static.EntryPoint{ + "bar": {Address: ":81"}, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/entrypoint-bar.json", + }, + }, + { + desc: "one entry point by id, that does not exist", + path: "/api/entrypoints/foo", + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: map[string]*static.EntryPoint{ + "bar": {Address: ":81"}, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one entry point by id, but no config", + path: "/api/entrypoints/foo", + conf: static.Configuration{API: &static.API{}, Global: &static.Global{}}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := New(test.conf, &dynamic.RuntimeConfiguration{}) + router := mux.NewRouter() + handler.Append(router) + + server := httptest.NewServer(router) + + resp, err := http.DefaultClient.Get(server.URL + test.path) + require.NoError(t, err) + + require.Equal(t, test.expected.statusCode, resp.StatusCode) + + assert.Equal(t, test.expected.nextPage, resp.Header.Get(nextPageHeader)) + + if test.expected.jsonFile == "" { + return + } + + assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") + contents, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + err = resp.Body.Close() + require.NoError(t, err) + + if *updateExpected { + var results interface{} + err := json.Unmarshal(contents, &results) + require.NoError(t, err) + + newJSON, err := json.MarshalIndent(results, "", "\t") + require.NoError(t, err) + + err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) + require.NoError(t, err) + } + + data, err := ioutil.ReadFile(test.expected.jsonFile) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(contents)) + }) + } +} + +func generateEntryPoints(nb int) map[string]*static.EntryPoint { + eps := make(map[string]*static.EntryPoint, nb) + for i := 0; i < nb; i++ { + eps[fmt.Sprintf("ep%2d", i)] = &static.EntryPoint{ + Address: ":" + strconv.Itoa(i), + } + } + + return eps +} diff --git a/pkg/api/handler_http.go b/pkg/api/handler_http.go new file mode 100644 index 000000000..02b8165f9 --- /dev/null +++ b/pkg/api/handler_http.go @@ -0,0 +1,198 @@ +package api + +import ( + "encoding/json" + "net/http" + "sort" + "strconv" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/log" +) + +type routerRepresentation struct { + *dynamic.RouterInfo + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` +} + +type serviceRepresentation struct { + *dynamic.ServiceInfo + ServerStatus map[string]string `json:"serverStatus,omitempty"` + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` +} + +type middlewareRepresentation struct { + *dynamic.MiddlewareInfo + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` +} + +func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) { + results := make([]routerRepresentation, 0, len(h.runtimeConfiguration.Routers)) + + for name, rt := range h.runtimeConfiguration.Routers { + results = append(results, routerRepresentation{ + RouterInfo: rt, + Name: name, + Provider: getProviderName(name), + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + pageInfo, err := pagination(request, len(results)) + if err != nil { + http.Error(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) + } +} + +func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) { + routerID := mux.Vars(request)["routerID"] + + router, ok := h.runtimeConfiguration.Routers[routerID] + if !ok { + http.NotFound(rw, request) + return + } + + result := routerRepresentation{ + RouterInfo: router, + Name: routerID, + Provider: getProviderName(routerID), + } + + 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) + } +} + +func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) { + results := make([]serviceRepresentation, 0, len(h.runtimeConfiguration.Services)) + + for name, si := range h.runtimeConfiguration.Services { + results = append(results, serviceRepresentation{ + ServiceInfo: si, + Name: name, + Provider: getProviderName(name), + ServerStatus: si.GetAllStatus(), + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + pageInfo, err := pagination(request, len(results)) + if err != nil { + http.Error(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) + } +} + +func (h Handler) getService(rw http.ResponseWriter, request *http.Request) { + serviceID := mux.Vars(request)["serviceID"] + + service, ok := h.runtimeConfiguration.Services[serviceID] + if !ok { + http.NotFound(rw, request) + return + } + + result := serviceRepresentation{ + ServiceInfo: service, + Name: serviceID, + Provider: getProviderName(serviceID), + ServerStatus: service.GetAllStatus(), + } + + rw.Header().Add("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) + } +} + +func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) { + results := make([]middlewareRepresentation, 0, len(h.runtimeConfiguration.Middlewares)) + + for name, mi := range h.runtimeConfiguration.Middlewares { + results = append(results, middlewareRepresentation{ + MiddlewareInfo: mi, + Name: name, + Provider: getProviderName(name), + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + pageInfo, err := pagination(request, len(results)) + if err != nil { + http.Error(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) + } +} + +func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) { + middlewareID := mux.Vars(request)["middlewareID"] + + middleware, ok := h.runtimeConfiguration.Middlewares[middlewareID] + if !ok { + http.NotFound(rw, request) + return + } + + result := middlewareRepresentation{ + MiddlewareInfo: middleware, + Name: middlewareID, + Provider: getProviderName(middlewareID), + } + + 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) + } +} diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go new file mode 100644 index 000000000..2d9a2235a --- /dev/null +++ b/pkg/api/handler_http_test.go @@ -0,0 +1,575 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/config/static" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandler_HTTP(t *testing.T) { + type expected struct { + statusCode int + nextPage string + jsonFile string + } + + testCases := []struct { + desc string + path string + conf dynamic.RuntimeConfiguration + expected expected + }{ + { + desc: "all routers, but no config", + path: "/api/http/routers", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/routers-empty.json", + }, + }, + { + desc: "all routers", + path: "/api/http/routers", + conf: dynamic.RuntimeConfiguration{ + Routers: map[string]*dynamic.RouterInfo{ + "test@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + }, + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/routers.json", + }, + }, + { + desc: "all routers, pagination, 1 res per page, want page 2", + path: "/api/http/routers?page=2&per_page=1", + conf: dynamic.RuntimeConfiguration{ + Routers: map[string]*dynamic.RouterInfo{ + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + }, + "baz@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`toto.bar`)", + }, + }, + "test@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/routers-page2.json", + }, + }, + { + desc: "all routers, pagination, 19 results overall, 7 res per page, want page 3", + path: "/api/http/routers?page=3&per_page=7", + conf: dynamic.RuntimeConfiguration{ + Routers: generateHTTPRouters(19), + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/routers-many-lastpage.json", + }, + }, + { + desc: "all routers, pagination, 5 results overall, 10 res per page, want page 2", + path: "/api/http/routers?page=2&per_page=10", + conf: dynamic.RuntimeConfiguration{ + Routers: generateHTTPRouters(5), + }, + expected: expected{ + statusCode: http.StatusBadRequest, + }, + }, + { + desc: "all routers, pagination, 10 results overall, 10 res per page, want page 2", + path: "/api/http/routers?page=2&per_page=10", + conf: dynamic.RuntimeConfiguration{ + Routers: generateHTTPRouters(10), + }, + expected: expected{ + statusCode: http.StatusBadRequest, + }, + }, + { + desc: "one router by id", + path: "/api/http/routers/bar@myprovider", + conf: dynamic.RuntimeConfiguration{ + Routers: map[string]*dynamic.RouterInfo{ + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/router-bar.json", + }, + }, + { + desc: "one router by id, that does not exist", + path: "/api/http/routers/foo@myprovider", + conf: dynamic.RuntimeConfiguration{ + Routers: map[string]*dynamic.RouterInfo{ + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one router by id, but no config", + path: "/api/http/routers/foo@myprovider", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "all services, but no config", + path: "/api/http/services", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/services-empty.json", + }, + }, + { + desc: "all services", + path: "/api/http/services", + conf: dynamic.RuntimeConfiguration{ + Services: map[string]*dynamic.ServiceInfo{ + "bar@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + } + si.UpdateStatus("http://127.0.0.1", "UP") + return si + }(), + "baz@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.2", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + } + si.UpdateStatus("http://127.0.0.2", "UP") + return si + }(), + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/services.json", + }, + }, + { + desc: "all services, 1 res per page, want page 2", + path: "/api/http/services?page=2&per_page=1", + conf: dynamic.RuntimeConfiguration{ + Services: map[string]*dynamic.ServiceInfo{ + "bar@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + } + si.UpdateStatus("http://127.0.0.1", "UP") + return si + }(), + "baz@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.2", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + } + si.UpdateStatus("http://127.0.0.2", "UP") + return si + }(), + "test@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.3", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + } + si.UpdateStatus("http://127.0.0.4", "UP") + return si + }(), + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/services-page2.json", + }, + }, + { + desc: "one service by id", + path: "/api/http/services/bar@myprovider", + conf: dynamic.RuntimeConfiguration{ + Services: map[string]*dynamic.ServiceInfo{ + "bar@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + } + si.UpdateStatus("http://127.0.0.1", "UP") + return si + }(), + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/service-bar.json", + }, + }, + { + desc: "one service by id, that does not exist", + path: "/api/http/services/nono@myprovider", + conf: dynamic.RuntimeConfiguration{ + Services: map[string]*dynamic.ServiceInfo{ + "bar@myprovider": func() *dynamic.ServiceInfo { + si := &dynamic.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + } + si.UpdateStatus("http://127.0.0.1", "UP") + return si + }(), + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one service by id, but no config", + path: "/api/http/services/foo@myprovider", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "all middlewares, but no config", + path: "/api/http/middlewares", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/middlewares-empty.json", + }, + }, + { + desc: "all middlewares", + path: "/api/http/middlewares", + conf: dynamic.RuntimeConfiguration{ + Middlewares: map[string]*dynamic.MiddlewareInfo{ + "auth@myprovider": { + Middleware: &dynamic.Middleware{ + BasicAuth: &dynamic.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + UsedBy: []string{"bar@myprovider", "test@myprovider"}, + }, + "addPrefixTest@myprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/titi", + }, + }, + UsedBy: []string{"test@myprovider"}, + }, + "addPrefixTest@anotherprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/toto", + }, + }, + UsedBy: []string{"bar@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/middlewares.json", + }, + }, + { + desc: "all middlewares, 1 res per page, want page 2", + path: "/api/http/middlewares?page=2&per_page=1", + conf: dynamic.RuntimeConfiguration{ + Middlewares: map[string]*dynamic.MiddlewareInfo{ + "auth@myprovider": { + Middleware: &dynamic.Middleware{ + BasicAuth: &dynamic.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + UsedBy: []string{"bar@myprovider", "test@myprovider"}, + }, + "addPrefixTest@myprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/titi", + }, + }, + UsedBy: []string{"test@myprovider"}, + }, + "addPrefixTest@anotherprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/toto", + }, + }, + UsedBy: []string{"bar@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/middlewares-page2.json", + }, + }, + { + desc: "one middleware by id", + path: "/api/http/middlewares/auth@myprovider", + conf: dynamic.RuntimeConfiguration{ + Middlewares: map[string]*dynamic.MiddlewareInfo{ + "auth@myprovider": { + Middleware: &dynamic.Middleware{ + BasicAuth: &dynamic.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + UsedBy: []string{"bar@myprovider", "test@myprovider"}, + }, + "addPrefixTest@myprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/titi", + }, + }, + UsedBy: []string{"test@myprovider"}, + }, + "addPrefixTest@anotherprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/toto", + }, + }, + UsedBy: []string{"bar@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/middleware-auth.json", + }, + }, + { + desc: "one middleware by id, that does not exist", + path: "/api/http/middlewares/foo@myprovider", + conf: dynamic.RuntimeConfiguration{ + Middlewares: map[string]*dynamic.MiddlewareInfo{ + "auth@myprovider": { + Middleware: &dynamic.Middleware{ + BasicAuth: &dynamic.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + UsedBy: []string{"bar@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one middleware by id, but no config", + path: "/api/http/middlewares/foo@myprovider", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rtConf := &test.conf + handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) + router := mux.NewRouter() + handler.Append(router) + + server := httptest.NewServer(router) + + resp, err := http.DefaultClient.Get(server.URL + test.path) + require.NoError(t, err) + + require.Equal(t, test.expected.statusCode, resp.StatusCode) + + assert.Equal(t, test.expected.nextPage, resp.Header.Get(nextPageHeader)) + + if test.expected.jsonFile == "" { + return + } + + assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") + contents, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + err = resp.Body.Close() + require.NoError(t, err) + + if *updateExpected { + var results interface{} + err := json.Unmarshal(contents, &results) + require.NoError(t, err) + + newJSON, err := json.MarshalIndent(results, "", "\t") + require.NoError(t, err) + + err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) + require.NoError(t, err) + } + + data, err := ioutil.ReadFile(test.expected.jsonFile) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(contents)) + }) + } +} + +func generateHTTPRouters(nbRouters int) map[string]*dynamic.RouterInfo { + routers := make(map[string]*dynamic.RouterInfo, nbRouters) + for i := 0; i < nbRouters; i++ { + routers[fmt.Sprintf("bar%2d@myprovider", i)] = &dynamic.RouterInfo{ + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar" + strconv.Itoa(i) + "`)", + }, + } + } + return routers +} diff --git a/pkg/api/handler_overview.go b/pkg/api/handler_overview.go new file mode 100644 index 000000000..01c15f2b5 --- /dev/null +++ b/pkg/api/handler_overview.go @@ -0,0 +1,200 @@ +package api + +import ( + "encoding/json" + "net/http" + "reflect" + + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/config/static" + "github.com/containous/traefik/pkg/log" +) + +type schemeOverview struct { + Routers *section `json:"routers,omitempty"` + Services *section `json:"services,omitempty"` + Middlewares *section `json:"middlewares,omitempty"` +} + +type section struct { + Total int `json:"total"` + Warnings int `json:"warnings"` + Errors int `json:"errors"` +} + +type features struct { + Tracing string `json:"tracing"` + Metrics string `json:"metrics"` + AccessLog bool `json:"accessLog"` + // TODO add certificates resolvers +} + +type overview struct { + HTTP schemeOverview `json:"http"` + TCP schemeOverview `json:"tcp"` + Features features `json:"features,omitempty"` + Providers []string `json:"providers,omitempty"` +} + +func (h Handler) getOverview(rw http.ResponseWriter, request *http.Request) { + result := overview{ + HTTP: schemeOverview{ + Routers: getHTTPRouterSection(h.runtimeConfiguration.Routers), + Services: getHTTPServiceSection(h.runtimeConfiguration.Services), + Middlewares: getHTTPMiddlewareSection(h.runtimeConfiguration.Middlewares), + }, + TCP: schemeOverview{ + Routers: getTCPRouterSection(h.runtimeConfiguration.TCPRouters), + Services: getTCPServiceSection(h.runtimeConfiguration.TCPServices), + }, + Features: getFeatures(h.staticConfig), + Providers: getProviders(h.staticConfig), + } + + 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) + } +} + +func getHTTPRouterSection(routers map[string]*dynamic.RouterInfo) *section { + var countErrors int + for _, rt := range routers { + if rt.Err != "" { + countErrors++ + } + } + + return §ion{ + Total: len(routers), + Warnings: 0, // TODO + Errors: countErrors, + } +} + +func getHTTPServiceSection(services map[string]*dynamic.ServiceInfo) *section { + var countErrors int + for _, svc := range services { + if svc.Err != nil { + countErrors++ + } + } + + return §ion{ + Total: len(services), + Warnings: 0, // TODO + Errors: countErrors, + } +} + +func getHTTPMiddlewareSection(middlewares map[string]*dynamic.MiddlewareInfo) *section { + var countErrors int + for _, md := range middlewares { + if md.Err != nil { + countErrors++ + } + } + + return §ion{ + Total: len(middlewares), + Warnings: 0, // TODO + Errors: countErrors, + } +} + +func getTCPRouterSection(routers map[string]*dynamic.TCPRouterInfo) *section { + var countErrors int + for _, rt := range routers { + if rt.Err != "" { + countErrors++ + } + } + + return §ion{ + Total: len(routers), + Warnings: 0, // TODO + Errors: countErrors, + } +} + +func getTCPServiceSection(services map[string]*dynamic.TCPServiceInfo) *section { + var countErrors int + for _, svc := range services { + if svc.Err != nil { + countErrors++ + } + } + + return §ion{ + Total: len(services), + Warnings: 0, // TODO + Errors: countErrors, + } +} + +func getProviders(conf static.Configuration) []string { + if conf.Providers == nil { + return nil + } + + var providers []string + + v := reflect.ValueOf(conf.Providers).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() { + providers = append(providers, v.Type().Field(i).Name) + } + } + } + + return providers +} + +func getFeatures(conf static.Configuration) features { + return features{ + Tracing: getTracing(conf), + Metrics: getMetrics(conf), + AccessLog: conf.AccessLog != nil, + } +} + +func getMetrics(conf static.Configuration) string { + if conf.Metrics == nil { + return "" + } + + v := reflect.ValueOf(conf.Metrics).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 "" +} + +func getTracing(conf static.Configuration) string { + if conf.Tracing == nil { + return "" + } + + v := reflect.ValueOf(conf.Tracing).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_overview_test.go b/pkg/api/handler_overview_test.go new file mode 100644 index 000000000..bfdaf0b42 --- /dev/null +++ b/pkg/api/handler_overview_test.go @@ -0,0 +1,230 @@ +package api + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/config/static" + "github.com/containous/traefik/pkg/provider/docker" + "github.com/containous/traefik/pkg/provider/file" + "github.com/containous/traefik/pkg/provider/kubernetes/crd" + "github.com/containous/traefik/pkg/provider/kubernetes/ingress" + "github.com/containous/traefik/pkg/provider/marathon" + "github.com/containous/traefik/pkg/provider/rancher" + "github.com/containous/traefik/pkg/provider/rest" + "github.com/containous/traefik/pkg/tracing/jaeger" + "github.com/containous/traefik/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandler_Overview(t *testing.T) { + type expected struct { + statusCode int + jsonFile string + } + + testCases := []struct { + desc string + path string + confStatic static.Configuration + confDyn dynamic.RuntimeConfiguration + expected expected + }{ + { + desc: "without data in the dynamic configuration", + path: "/api/overview", + confStatic: static.Configuration{API: &static.API{}, Global: &static.Global{}}, + confDyn: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/overview-empty.json", + }, + }, + { + desc: "with data in the dynamic configuration", + path: "/api/overview", + confStatic: static.Configuration{API: &static.API{}, Global: &static.Global{}}, + confDyn: dynamic.RuntimeConfiguration{ + Services: map[string]*dynamic.ServiceInfo{ + "foo-service@myprovider": { + Service: &dynamic.Service{ + LoadBalancer: &dynamic.LoadBalancerService{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1", + }, + }, + }, + }, + }, + }, + Middlewares: map[string]*dynamic.MiddlewareInfo{ + "auth@myprovider": { + Middleware: &dynamic.Middleware{ + BasicAuth: &dynamic.BasicAuth{ + Users: []string{"admin:admin"}, + }, + }, + }, + "addPrefixTest@myprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/titi", + }, + }, + }, + "addPrefixTest@anotherprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/toto", + }, + }, + }, + }, + Routers: map[string]*dynamic.RouterInfo{ + "bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + }, + "test@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + Middlewares: []string{"addPrefixTest", "auth"}, + }, + }, + }, + TCPServices: map[string]*dynamic.TCPServiceInfo{ + "tcpfoo-service@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1", + }, + }, + }, + }, + }, + }, + TCPRouters: map[string]*dynamic.TCPRouterInfo{ + "tcpbar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "tcpfoo-service@myprovider", + Rule: "HostSNI(`foo.bar`)", + }, + }, + "tcptest@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "tcpfoo-service@myprovider", + Rule: "HostSNI(`foo.bar.other`)", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/overview-dynamic.json", + }, + }, + { + desc: "with providers", + path: "/api/overview", + confStatic: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + Providers: &static.Providers{ + Docker: &docker.Provider{}, + File: &file.Provider{}, + Marathon: &marathon.Provider{}, + KubernetesIngress: &ingress.Provider{}, + KubernetesCRD: &crd.Provider{}, + Rest: &rest.Provider{}, + Rancher: &rancher.Provider{}, + }, + }, + confDyn: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/overview-providers.json", + }, + }, + { + desc: "with features", + path: "/api/overview", + confStatic: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + Metrics: &types.Metrics{ + Prometheus: &types.Prometheus{}, + }, + Tracing: &static.Tracing{ + Jaeger: &jaeger.Config{}, + }, + }, + confDyn: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/overview-features.json", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := New(test.confStatic, &test.confDyn) + router := mux.NewRouter() + handler.Append(router) + + server := httptest.NewServer(router) + + resp, err := http.DefaultClient.Get(server.URL + test.path) + require.NoError(t, err) + + require.Equal(t, test.expected.statusCode, resp.StatusCode) + + if test.expected.jsonFile == "" { + return + } + + assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") + contents, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + err = resp.Body.Close() + require.NoError(t, err) + + if *updateExpected { + var results interface{} + err := json.Unmarshal(contents, &results) + require.NoError(t, err) + + newJSON, err := json.MarshalIndent(results, "", "\t") + require.NoError(t, err) + + err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) + require.NoError(t, err) + } + + data, err := ioutil.ReadFile(test.expected.jsonFile) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(contents)) + }) + } +} diff --git a/pkg/api/handler_tcp.go b/pkg/api/handler_tcp.go new file mode 100644 index 000000000..81217f240 --- /dev/null +++ b/pkg/api/handler_tcp.go @@ -0,0 +1,134 @@ +package api + +import ( + "encoding/json" + "net/http" + "sort" + "strconv" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/log" +) + +type tcpRouterRepresentation struct { + *dynamic.TCPRouterInfo + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` +} + +type tcpServiceRepresentation struct { + *dynamic.TCPServiceInfo + Name string `json:"name,omitempty"` + Provider string `json:"provider,omitempty"` +} + +func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) { + results := make([]tcpRouterRepresentation, 0, len(h.runtimeConfiguration.TCPRouters)) + + for name, rt := range h.runtimeConfiguration.TCPRouters { + results = append(results, tcpRouterRepresentation{ + TCPRouterInfo: rt, + Name: name, + Provider: getProviderName(name), + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + pageInfo, err := pagination(request, len(results)) + if err != nil { + http.Error(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) + } +} + +func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) { + routerID := mux.Vars(request)["routerID"] + + router, ok := h.runtimeConfiguration.TCPRouters[routerID] + if !ok { + http.NotFound(rw, request) + return + } + + result := tcpRouterRepresentation{ + TCPRouterInfo: router, + Name: routerID, + Provider: getProviderName(routerID), + } + + 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) + } +} + +func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) { + results := make([]tcpServiceRepresentation, 0, len(h.runtimeConfiguration.TCPServices)) + + for name, si := range h.runtimeConfiguration.TCPServices { + results = append(results, tcpServiceRepresentation{ + TCPServiceInfo: si, + Name: name, + Provider: getProviderName(name), + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + pageInfo, err := pagination(request, len(results)) + if err != nil { + http.Error(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) + } +} + +func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) { + serviceID := mux.Vars(request)["serviceID"] + + service, ok := h.runtimeConfiguration.TCPServices[serviceID] + if !ok { + http.NotFound(rw, request) + return + } + + result := tcpServiceRepresentation{ + TCPServiceInfo: service, + Name: serviceID, + Provider: getProviderName(serviceID), + } + + 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) + } +} diff --git a/pkg/api/handler_tcp_test.go b/pkg/api/handler_tcp_test.go new file mode 100644 index 000000000..20f7cbeb8 --- /dev/null +++ b/pkg/api/handler_tcp_test.go @@ -0,0 +1,349 @@ +package api + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containous/mux" + "github.com/containous/traefik/pkg/config/dynamic" + "github.com/containous/traefik/pkg/config/static" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandler_TCP(t *testing.T) { + type expected struct { + statusCode int + nextPage string + jsonFile string + } + + testCases := []struct { + desc string + path string + conf dynamic.RuntimeConfiguration + expected expected + }{ + { + desc: "all TCP routers, but no config", + path: "/api/tcp/routers", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/tcprouters-empty.json", + }, + }, + { + desc: "all TCP routers", + path: "/api/tcp/routers", + conf: dynamic.RuntimeConfiguration{ + TCPRouters: map[string]*dynamic.TCPRouterInfo{ + "test@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Passthrough: false, + }, + }, + }, + "bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/tcprouters.json", + }, + }, + { + desc: "all TCP routers, pagination, 1 res per page, want page 2", + path: "/api/tcp/routers?page=2&per_page=1", + conf: dynamic.RuntimeConfiguration{ + TCPRouters: map[string]*dynamic.TCPRouterInfo{ + "bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + }, + }, + "baz@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`toto.bar`)", + }, + }, + "test@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar.other`)", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/tcprouters-page2.json", + }, + }, + { + desc: "one TCP router by id", + path: "/api/tcp/routers/bar@myprovider", + conf: dynamic.RuntimeConfiguration{ + TCPRouters: map[string]*dynamic.TCPRouterInfo{ + "bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/tcprouter-bar.json", + }, + }, + { + desc: "one TCP router by id, that does not exist", + path: "/api/tcp/routers/foo@myprovider", + conf: dynamic.RuntimeConfiguration{ + TCPRouters: map[string]*dynamic.TCPRouterInfo{ + "bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one TCP router by id, but no config", + path: "/api/tcp/routers/bar@myprovider", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "all tcp services, but no config", + path: "/api/tcp/services", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/tcpservices-empty.json", + }, + }, + { + desc: "all tcp services", + path: "/api/tcp/services", + conf: dynamic.RuntimeConfiguration{ + TCPServices: map[string]*dynamic.TCPServiceInfo{ + "bar@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + "baz@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "1", + jsonFile: "testdata/tcpservices.json", + }, + }, + { + desc: "all tcp services, 1 res per page, want page 2", + path: "/api/tcp/services?page=2&per_page=1", + conf: dynamic.RuntimeConfiguration{ + TCPServices: map[string]*dynamic.TCPServiceInfo{ + "bar@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + "baz@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.2:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider"}, + }, + "test@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.3:2345", + }, + }, + }, + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + nextPage: "3", + jsonFile: "testdata/tcpservices-page2.json", + }, + }, + { + desc: "one tcp service by id", + path: "/api/tcp/services/bar@myprovider", + conf: dynamic.RuntimeConfiguration{ + TCPServices: map[string]*dynamic.TCPServiceInfo{ + "bar@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/tcpservice-bar.json", + }, + }, + { + desc: "one tcp service by id, that does not exist", + path: "/api/tcp/services/nono@myprovider", + conf: dynamic.RuntimeConfiguration{ + TCPServices: map[string]*dynamic.TCPServiceInfo{ + "bar@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + { + desc: "one tcp service by id, but no config", + path: "/api/tcp/services/foo@myprovider", + conf: dynamic.RuntimeConfiguration{}, + expected: expected{ + statusCode: http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rtConf := &test.conf + handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) + router := mux.NewRouter() + handler.Append(router) + + server := httptest.NewServer(router) + + resp, err := http.DefaultClient.Get(server.URL + test.path) + require.NoError(t, err) + + assert.Equal(t, test.expected.nextPage, resp.Header.Get(nextPageHeader)) + + require.Equal(t, test.expected.statusCode, resp.StatusCode) + + if test.expected.jsonFile == "" { + return + } + + assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") + + contents, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + err = resp.Body.Close() + require.NoError(t, err) + + if *updateExpected { + var results interface{} + err := json.Unmarshal(contents, &results) + require.NoError(t, err) + + newJSON, err := json.MarshalIndent(results, "", "\t") + require.NoError(t, err) + + err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) + require.NoError(t, err) + } + + data, err := ioutil.ReadFile(test.expected.jsonFile) + require.NoError(t, err) + assert.JSONEq(t, string(data), string(contents)) + }) + } +} diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go index fa770b33a..59f0d5edc 100644 --- a/pkg/api/handler_test.go +++ b/pkg/api/handler_test.go @@ -3,11 +3,9 @@ package api import ( "encoding/json" "flag" - "fmt" "io/ioutil" "net/http" "net/http/httptest" - "strconv" "testing" "github.com/containous/mux" @@ -19,885 +17,7 @@ import ( var updateExpected = flag.Bool("update_expected", false, "Update expected files in testdata") -func TestHandlerTCP_API(t *testing.T) { - type expected struct { - statusCode int - nextPage string - jsonFile string - } - - testCases := []struct { - desc string - path string - conf dynamic.RuntimeConfiguration - expected expected - }{ - { - desc: "all TCP routers, but no config", - path: "/api/tcp/routers", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/tcprouters-empty.json", - }, - }, - { - desc: "all TCP routers", - path: "/api/tcp/routers", - conf: dynamic.RuntimeConfiguration{ - TCPRouters: map[string]*dynamic.TCPRouterInfo{ - "test@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar.other`)", - TLS: &dynamic.RouterTCPTLSConfig{ - Passthrough: false, - }, - }, - }, - "bar@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/tcprouters.json", - }, - }, - { - desc: "all TCP routers, pagination, 1 res per page, want page 2", - path: "/api/tcp/routers?page=2&per_page=1", - conf: dynamic.RuntimeConfiguration{ - TCPRouters: map[string]*dynamic.TCPRouterInfo{ - "bar@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - }, - }, - "baz@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`toto.bar`)", - }, - }, - "test@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar.other`)", - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "3", - jsonFile: "testdata/tcprouters-page2.json", - }, - }, - { - desc: "one TCP router by id", - path: "/api/tcp/routers/bar@myprovider", - conf: dynamic.RuntimeConfiguration{ - TCPRouters: map[string]*dynamic.TCPRouterInfo{ - "bar@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - jsonFile: "testdata/tcprouter-bar.json", - }, - }, - { - desc: "one TCP router by id, that does not exist", - path: "/api/tcp/routers/foo@myprovider", - conf: dynamic.RuntimeConfiguration{ - TCPRouters: map[string]*dynamic.TCPRouterInfo{ - "bar@myprovider": { - TCPRouter: &dynamic.TCPRouter{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "one TCP router by id, but no config", - path: "/api/tcp/routers/bar@myprovider", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "all tcp services, but no config", - path: "/api/tcp/services", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/tcpservices-empty.json", - }, - }, - { - desc: "all tcp services", - path: "/api/tcp/services", - conf: dynamic.RuntimeConfiguration{ - TCPServices: map[string]*dynamic.TCPServiceInfo{ - "bar@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.1:2345", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - }, - "baz@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.2:2345", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/tcpservices.json", - }, - }, - { - desc: "all tcp services, 1 res per page, want page 2", - path: "/api/tcp/services?page=2&per_page=1", - conf: dynamic.RuntimeConfiguration{ - TCPServices: map[string]*dynamic.TCPServiceInfo{ - "bar@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.1:2345", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - }, - "baz@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.2:2345", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider"}, - }, - "test@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.3:2345", - }, - }, - }, - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "3", - jsonFile: "testdata/tcpservices-page2.json", - }, - }, - { - desc: "one tcp service by id", - path: "/api/tcp/services/bar@myprovider", - conf: dynamic.RuntimeConfiguration{ - TCPServices: map[string]*dynamic.TCPServiceInfo{ - "bar@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.1:2345", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - jsonFile: "testdata/tcpservice-bar.json", - }, - }, - { - desc: "one tcp service by id, that does not exist", - path: "/api/tcp/services/nono@myprovider", - conf: dynamic.RuntimeConfiguration{ - TCPServices: map[string]*dynamic.TCPServiceInfo{ - "bar@myprovider": { - TCPService: &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: []dynamic.TCPServer{ - { - Address: "127.0.0.1:2345", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "one tcp service by id, but no config", - path: "/api/tcp/services/foo@myprovider", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rtConf := &test.conf - handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) - - resp, err := http.DefaultClient.Get(server.URL + test.path) - require.NoError(t, err) - - assert.Equal(t, test.expected.nextPage, resp.Header.Get(nextPageHeader)) - - require.Equal(t, test.expected.statusCode, resp.StatusCode) - - if test.expected.jsonFile == "" { - return - } - - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - - contents, err := ioutil.ReadAll(resp.Body) - require.NoError(t, err) - - err = resp.Body.Close() - require.NoError(t, err) - - if *updateExpected { - var results interface{} - err := json.Unmarshal(contents, &results) - require.NoError(t, err) - - newJSON, err := json.MarshalIndent(results, "", "\t") - require.NoError(t, err) - - err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) - require.NoError(t, err) - } - - data, err := ioutil.ReadFile(test.expected.jsonFile) - require.NoError(t, err) - assert.JSONEq(t, string(data), string(contents)) - }) - } -} - -func TestHandlerHTTP_API(t *testing.T) { - type expected struct { - statusCode int - nextPage string - jsonFile string - } - - testCases := []struct { - desc string - path string - conf dynamic.RuntimeConfiguration - expected expected - }{ - { - desc: "all routers, but no config", - path: "/api/http/routers", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/routers-empty.json", - }, - }, - { - desc: "all routers", - path: "/api/http/routers", - conf: dynamic.RuntimeConfiguration{ - Routers: map[string]*dynamic.RouterInfo{ - "test@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar.other`)", - Middlewares: []string{"addPrefixTest", "auth"}, - }, - }, - "bar@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/routers.json", - }, - }, - { - desc: "all routers, pagination, 1 res per page, want page 2", - path: "/api/http/routers?page=2&per_page=1", - conf: dynamic.RuntimeConfiguration{ - Routers: map[string]*dynamic.RouterInfo{ - "bar@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, - }, - }, - "baz@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`toto.bar`)", - }, - }, - "test@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar.other`)", - Middlewares: []string{"addPrefixTest", "auth"}, - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "3", - jsonFile: "testdata/routers-page2.json", - }, - }, - { - desc: "all routers, pagination, 19 results overall, 7 res per page, want page 3", - path: "/api/http/routers?page=3&per_page=7", - conf: dynamic.RuntimeConfiguration{ - Routers: generateHTTPRouters(19), - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/routers-many-lastpage.json", - }, - }, - { - desc: "all routers, pagination, 5 results overall, 10 res per page, want page 2", - path: "/api/http/routers?page=2&per_page=10", - conf: dynamic.RuntimeConfiguration{ - Routers: generateHTTPRouters(5), - }, - expected: expected{ - statusCode: http.StatusBadRequest, - }, - }, - { - desc: "all routers, pagination, 10 results overall, 10 res per page, want page 2", - path: "/api/http/routers?page=2&per_page=10", - conf: dynamic.RuntimeConfiguration{ - Routers: generateHTTPRouters(10), - }, - expected: expected{ - statusCode: http.StatusBadRequest, - }, - }, - { - desc: "one router by id", - path: "/api/http/routers/bar@myprovider", - conf: dynamic.RuntimeConfiguration{ - Routers: map[string]*dynamic.RouterInfo{ - "bar@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - jsonFile: "testdata/router-bar.json", - }, - }, - { - desc: "one router by id, that does not exist", - path: "/api/http/routers/foo@myprovider", - conf: dynamic.RuntimeConfiguration{ - Routers: map[string]*dynamic.RouterInfo{ - "bar@myprovider": { - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar`)", - Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, - }, - }, - }, - }, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "one router by id, but no config", - path: "/api/http/routers/foo@myprovider", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "all services, but no config", - path: "/api/http/services", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/services-empty.json", - }, - }, - { - desc: "all services", - path: "/api/http/services", - conf: dynamic.RuntimeConfiguration{ - Services: map[string]*dynamic.ServiceInfo{ - "bar@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.1", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - } - si.UpdateStatus("http://127.0.0.1", "UP") - return si - }(), - "baz@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.2", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider"}, - } - si.UpdateStatus("http://127.0.0.2", "UP") - return si - }(), - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/services.json", - }, - }, - { - desc: "all services, 1 res per page, want page 2", - path: "/api/http/services?page=2&per_page=1", - conf: dynamic.RuntimeConfiguration{ - Services: map[string]*dynamic.ServiceInfo{ - "bar@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.1", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - } - si.UpdateStatus("http://127.0.0.1", "UP") - return si - }(), - "baz@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.2", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider"}, - } - si.UpdateStatus("http://127.0.0.2", "UP") - return si - }(), - "test@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.3", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - } - si.UpdateStatus("http://127.0.0.4", "UP") - return si - }(), - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "3", - jsonFile: "testdata/services-page2.json", - }, - }, - { - desc: "one service by id", - path: "/api/http/services/bar@myprovider", - conf: dynamic.RuntimeConfiguration{ - Services: map[string]*dynamic.ServiceInfo{ - "bar@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.1", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - } - si.UpdateStatus("http://127.0.0.1", "UP") - return si - }(), - }, - }, - expected: expected{ - statusCode: http.StatusOK, - jsonFile: "testdata/service-bar.json", - }, - }, - { - desc: "one service by id, that does not exist", - path: "/api/http/services/nono@myprovider", - conf: dynamic.RuntimeConfiguration{ - Services: map[string]*dynamic.ServiceInfo{ - "bar@myprovider": func() *dynamic.ServiceInfo { - si := &dynamic.ServiceInfo{ - Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: []dynamic.Server{ - { - URL: "http://127.0.0.1", - }, - }, - }, - }, - UsedBy: []string{"foo@myprovider", "test@myprovider"}, - } - si.UpdateStatus("http://127.0.0.1", "UP") - return si - }(), - }, - }, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "one service by id, but no config", - path: "/api/http/services/foo@myprovider", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "all middlewares, but no config", - path: "/api/http/middlewares", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/middlewares-empty.json", - }, - }, - { - desc: "all middlewares", - path: "/api/http/middlewares", - conf: dynamic.RuntimeConfiguration{ - Middlewares: map[string]*dynamic.MiddlewareInfo{ - "auth@myprovider": { - Middleware: &dynamic.Middleware{ - BasicAuth: &dynamic.BasicAuth{ - Users: []string{"admin:admin"}, - }, - }, - UsedBy: []string{"bar@myprovider", "test@myprovider"}, - }, - "addPrefixTest@myprovider": { - Middleware: &dynamic.Middleware{ - AddPrefix: &dynamic.AddPrefix{ - Prefix: "/titi", - }, - }, - UsedBy: []string{"test@myprovider"}, - }, - "addPrefixTest@anotherprovider": { - Middleware: &dynamic.Middleware{ - AddPrefix: &dynamic.AddPrefix{ - Prefix: "/toto", - }, - }, - UsedBy: []string{"bar@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "1", - jsonFile: "testdata/middlewares.json", - }, - }, - { - desc: "all middlewares, 1 res per page, want page 2", - path: "/api/http/middlewares?page=2&per_page=1", - conf: dynamic.RuntimeConfiguration{ - Middlewares: map[string]*dynamic.MiddlewareInfo{ - "auth@myprovider": { - Middleware: &dynamic.Middleware{ - BasicAuth: &dynamic.BasicAuth{ - Users: []string{"admin:admin"}, - }, - }, - UsedBy: []string{"bar@myprovider", "test@myprovider"}, - }, - "addPrefixTest@myprovider": { - Middleware: &dynamic.Middleware{ - AddPrefix: &dynamic.AddPrefix{ - Prefix: "/titi", - }, - }, - UsedBy: []string{"test@myprovider"}, - }, - "addPrefixTest@anotherprovider": { - Middleware: &dynamic.Middleware{ - AddPrefix: &dynamic.AddPrefix{ - Prefix: "/toto", - }, - }, - UsedBy: []string{"bar@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - nextPage: "3", - jsonFile: "testdata/middlewares-page2.json", - }, - }, - { - desc: "one middleware by id", - path: "/api/http/middlewares/auth@myprovider", - conf: dynamic.RuntimeConfiguration{ - Middlewares: map[string]*dynamic.MiddlewareInfo{ - "auth@myprovider": { - Middleware: &dynamic.Middleware{ - BasicAuth: &dynamic.BasicAuth{ - Users: []string{"admin:admin"}, - }, - }, - UsedBy: []string{"bar@myprovider", "test@myprovider"}, - }, - "addPrefixTest@myprovider": { - Middleware: &dynamic.Middleware{ - AddPrefix: &dynamic.AddPrefix{ - Prefix: "/titi", - }, - }, - UsedBy: []string{"test@myprovider"}, - }, - "addPrefixTest@anotherprovider": { - Middleware: &dynamic.Middleware{ - AddPrefix: &dynamic.AddPrefix{ - Prefix: "/toto", - }, - }, - UsedBy: []string{"bar@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusOK, - jsonFile: "testdata/middleware-auth.json", - }, - }, - { - desc: "one middleware by id, that does not exist", - path: "/api/http/middlewares/foo@myprovider", - conf: dynamic.RuntimeConfiguration{ - Middlewares: map[string]*dynamic.MiddlewareInfo{ - "auth@myprovider": { - Middleware: &dynamic.Middleware{ - BasicAuth: &dynamic.BasicAuth{ - Users: []string{"admin:admin"}, - }, - }, - UsedBy: []string{"bar@myprovider", "test@myprovider"}, - }, - }, - }, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - { - desc: "one middleware by id, but no config", - path: "/api/http/middlewares/foo@myprovider", - conf: dynamic.RuntimeConfiguration{}, - expected: expected{ - statusCode: http.StatusNotFound, - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rtConf := &test.conf - handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) - router := mux.NewRouter() - handler.Append(router) - - server := httptest.NewServer(router) - - resp, err := http.DefaultClient.Get(server.URL + test.path) - require.NoError(t, err) - - require.Equal(t, test.expected.statusCode, resp.StatusCode) - - assert.Equal(t, test.expected.nextPage, resp.Header.Get(nextPageHeader)) - - if test.expected.jsonFile == "" { - return - } - - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - contents, err := ioutil.ReadAll(resp.Body) - require.NoError(t, err) - - err = resp.Body.Close() - require.NoError(t, err) - - if *updateExpected { - var results interface{} - err := json.Unmarshal(contents, &results) - require.NoError(t, err) - - newJSON, err := json.MarshalIndent(results, "", "\t") - require.NoError(t, err) - - err = ioutil.WriteFile(test.expected.jsonFile, newJSON, 0644) - require.NoError(t, err) - } - - data, err := ioutil.ReadFile(test.expected.jsonFile) - require.NoError(t, err) - assert.JSONEq(t, string(data), string(contents)) - }) - } -} - -func TestHandler_Configuration(t *testing.T) { +func TestHandler_RawData(t *testing.T) { type expected struct { statusCode int json string @@ -1053,17 +173,3 @@ func TestHandler_Configuration(t *testing.T) { }) } } - -func generateHTTPRouters(nbRouters int) map[string]*dynamic.RouterInfo { - routers := make(map[string]*dynamic.RouterInfo, nbRouters) - for i := 0; i < nbRouters; i++ { - routers[fmt.Sprintf("bar%2d@myprovider", i)] = &dynamic.RouterInfo{ - Router: &dynamic.Router{ - EntryPoints: []string{"web"}, - Service: "foo-service@myprovider", - Rule: "Host(`foo.bar" + strconv.Itoa(i) + "`)", - }, - } - } - return routers -} diff --git a/pkg/api/testdata/entrypoint-bar.json b/pkg/api/testdata/entrypoint-bar.json new file mode 100644 index 000000000..31177544f --- /dev/null +++ b/pkg/api/testdata/entrypoint-bar.json @@ -0,0 +1,4 @@ +{ + "address": ":81", + "name": "bar" +} \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints-empty.json b/pkg/api/testdata/entrypoints-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/api/testdata/entrypoints-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints-many-lastpage.json b/pkg/api/testdata/entrypoints-many-lastpage.json new file mode 100644 index 000000000..588b0e2e3 --- /dev/null +++ b/pkg/api/testdata/entrypoints-many-lastpage.json @@ -0,0 +1,22 @@ +[ + { + "address": ":14", + "name": "ep14" + }, + { + "address": ":15", + "name": "ep15" + }, + { + "address": ":16", + "name": "ep16" + }, + { + "address": ":17", + "name": "ep17" + }, + { + "address": ":18", + "name": "ep18" + } +] \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints-page2.json b/pkg/api/testdata/entrypoints-page2.json new file mode 100644 index 000000000..142f5b9e7 --- /dev/null +++ b/pkg/api/testdata/entrypoints-page2.json @@ -0,0 +1,6 @@ +[ + { + "address": ":82", + "name": "web2" + } +] \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints.json b/pkg/api/testdata/entrypoints.json new file mode 100644 index 000000000..4c8e5ae55 --- /dev/null +++ b/pkg/api/testdata/entrypoints.json @@ -0,0 +1,60 @@ +[ + { + "address": ":80", + "forwardedHeaders": { + "insecure": true, + "trustedIPs": [ + "192.168.1.3", + "192.168.1.4" + ] + }, + "name": "web", + "proxyProtocol": { + "insecure": true, + "trustedIPs": [ + "192.168.1.1", + "192.168.1.2" + ] + }, + "transport": { + "lifeCycle": { + "graceTimeOut": 2, + "requestAcceptGraceTimeout": 1 + }, + "respondingTimeouts": { + "idleTimeout": 5, + "readTimeout": 3, + "writeTimeout": 4 + } + } + }, + { + "address": ":443", + "forwardedHeaders": { + "insecure": true, + "trustedIPs": [ + "192.168.1.30", + "192.168.1.40" + ] + }, + "name": "web-secure", + "proxyProtocol": { + "insecure": true, + "trustedIPs": [ + "192.168.1.10", + "192.168.1.20" + ] + }, + "transport": { + "lifeCycle": { + "graceTimeOut": 20, + "requestAcceptGraceTimeout": 10 + }, + "respondingTimeouts": { + "idleTimeout": 50, + "readTimeout": 30, + "writeTimeout": 40 + } + } + } +] \ No newline at end of file diff --git a/pkg/api/testdata/overview-dynamic.json b/pkg/api/testdata/overview-dynamic.json new file mode 100644 index 000000000..346f47433 --- /dev/null +++ b/pkg/api/testdata/overview-dynamic.json @@ -0,0 +1,36 @@ +{ + "features": { + "accessLog": false, + "metrics": "", + "tracing": "" + }, + "http": { + "middlewares": { + "errors": 0, + "total": 3, + "warnings": 0 + }, + "routers": { + "errors": 0, + "total": 2, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 1, + "warnings": 0 + } + }, + "tcp": { + "routers": { + "errors": 0, + "total": 2, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 1, + "warnings": 0 + } + } +} \ No newline at end of file diff --git a/pkg/api/testdata/overview-empty.json b/pkg/api/testdata/overview-empty.json new file mode 100644 index 000000000..974fa846a --- /dev/null +++ b/pkg/api/testdata/overview-empty.json @@ -0,0 +1,36 @@ +{ + "features": { + "accessLog": false, + "metrics": "", + "tracing": "" + }, + "http": { + "middlewares": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "routers": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 0, + "warnings": 0 + } + }, + "tcp": { + "routers": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 0, + "warnings": 0 + } + } +} \ No newline at end of file diff --git a/pkg/api/testdata/overview-features.json b/pkg/api/testdata/overview-features.json new file mode 100644 index 000000000..91b0c1c45 --- /dev/null +++ b/pkg/api/testdata/overview-features.json @@ -0,0 +1,36 @@ +{ + "features": { + "accessLog": false, + "metrics": "Prometheus", + "tracing": "Jaeger" + }, + "http": { + "middlewares": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "routers": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 0, + "warnings": 0 + } + }, + "tcp": { + "routers": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 0, + "warnings": 0 + } + } +} \ No newline at end of file diff --git a/pkg/api/testdata/overview-providers.json b/pkg/api/testdata/overview-providers.json new file mode 100644 index 000000000..516194468 --- /dev/null +++ b/pkg/api/testdata/overview-providers.json @@ -0,0 +1,45 @@ +{ + "features": { + "accessLog": false, + "metrics": "", + "tracing": "" + }, + "http": { + "middlewares": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "routers": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 0, + "warnings": 0 + } + }, + "providers": [ + "Docker", + "File", + "Marathon", + "KubernetesIngress", + "KubernetesCRD", + "Rest", + "Rancher" + ], + "tcp": { + "routers": { + "errors": 0, + "total": 0, + "warnings": 0 + }, + "services": { + "errors": 0, + "total": 0, + "warnings": 0 + } + } +} \ No newline at end of file