Improve API endpoints

This commit is contained in:
Ludovic Fernandez 2019-07-12 11:10:03 +02:00 committed by Traefiker Bot
parent c8bf8e896a
commit 74c5ec70a9
20 changed files with 2266 additions and 1204 deletions

View file

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

View file

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

View file

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

View file

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

198
pkg/api/handler_http.go Normal file
View file

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

View file

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

200
pkg/api/handler_overview.go Normal file
View file

@ -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 &section{
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 &section{
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 &section{
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 &section{
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 &section{
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 ""
}

View file

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

134
pkg/api/handler_tcp.go Normal file
View file

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

349
pkg/api/handler_tcp_test.go Normal file
View file

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

View file

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

4
pkg/api/testdata/entrypoint-bar.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"address": ":81",
"name": "bar"
}

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,22 @@
[
{
"address": ":14",
"name": "ep14"
},
{
"address": ":15",
"name": "ep15"
},
{
"address": ":16",
"name": "ep16"
},
{
"address": ":17",
"name": "ep17"
},
{
"address": ":18",
"name": "ep18"
}
]

View file

@ -0,0 +1,6 @@
[
{
"address": ":82",
"name": "web2"
}
]

60
pkg/api/testdata/entrypoints.json vendored Normal file
View file

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

36
pkg/api/testdata/overview-dynamic.json vendored Normal file
View file

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

36
pkg/api/testdata/overview-empty.json vendored Normal file
View file

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

36
pkg/api/testdata/overview-features.json vendored Normal file
View file

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

View file

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