1
0
Fork 0

API: new contract

Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
mpl 2019-06-19 18:34:04 +02:00 committed by Traefiker Bot
parent a34876d700
commit 429b1d8574
34 changed files with 1810 additions and 61 deletions

View file

@ -1,8 +1,12 @@
package api
import (
"io"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"github.com/containous/mux"
"github.com/containous/traefik/pkg/config"
@ -11,14 +15,14 @@ import (
"github.com/containous/traefik/pkg/types"
"github.com/containous/traefik/pkg/version"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/unrolled/render"
)
var templateRenderer jsonRenderer = render.New(render.Options{Directory: "nowhere"})
const (
defaultPerPage = 100
defaultPage = 1
)
type jsonRenderer interface {
JSON(w io.Writer, status int, v interface{}) error
}
const nextPageHeader = "X-Next-Page"
type serviceInfoRepresentation struct {
*config.ServiceInfo
@ -34,6 +38,43 @@ type RunTimeRepresentation struct {
TCPServices map[string]*config.TCPServiceInfo `json:"tcpServices,omitempty"`
}
type routerRepresentation struct {
*config.RouterInfo
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
}
type serviceRepresentation struct {
*config.ServiceInfo
ServerStatus map[string]string `json:"serverStatus,omitempty"`
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
}
type middlewareRepresentation struct {
*config.MiddlewareInfo
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
}
type tcpRouterRepresentation struct {
*config.TCPRouterInfo
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
}
type tcpServiceRepresentation struct {
*config.TCPServiceInfo
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
}
type pageInfo struct {
startIndex int
endIndex int
nextPage int
}
// Handler serves the configuration and status of Traefik on API endpoints.
type Handler struct {
dashboard bool
@ -59,7 +100,7 @@ func New(staticConfig static.Configuration, runtimeConfig *config.RuntimeConfigu
statistics: staticConfig.API.Statistics,
dashboardAssets: staticConfig.API.DashboardAssets,
runtimeConfiguration: rConfig,
debug: staticConfig.Global.Debug,
debug: staticConfig.API.Debug,
}
}
@ -71,9 +112,21 @@ func (h Handler) Append(router *mux.Router) {
router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration)
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)
router.Methods(http.MethodGet).Path("/api/http/services/{serviceID}").HandlerFunc(h.getService)
router.Methods(http.MethodGet).Path("/api/http/middlewares").HandlerFunc(h.getMiddlewares)
router.Methods(http.MethodGet).Path("/api/http/middlewares/{middlewareID}").HandlerFunc(h.getMiddleware)
router.Methods(http.MethodGet).Path("/api/tcp/routers").HandlerFunc(h.getTCPRouters)
router.Methods(http.MethodGet).Path("/api/tcp/routers/{routerID}").HandlerFunc(h.getTCPRouter)
router.Methods(http.MethodGet).Path("/api/tcp/services").HandlerFunc(h.getTCPServices)
router.Methods(http.MethodGet).Path("/api/tcp/services/{serviceID}").HandlerFunc(h.getTCPService)
// FIXME stats
// health route
//router.Methods(http.MethodGet).Path("/health").HandlerFunc(p.getHealthHandler)
// router.Methods(http.MethodGet).Path("/health").HandlerFunc(p.getHealthHandler)
version.Handler{}.Append(router)
@ -82,6 +135,268 @@ 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(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),
}
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(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(),
}
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(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),
}
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(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),
}
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(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),
}
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 {
@ -91,7 +406,7 @@ func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.R
}
}
rtRepr := RunTimeRepresentation{
result := RunTimeRepresentation{
Routers: h.runtimeConfiguration.Routers,
Middlewares: h.runtimeConfiguration.Middlewares,
Services: siRepr,
@ -99,9 +414,55 @@ func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.R
TCPServices: h.runtimeConfiguration.TCPServices,
}
err := templateRenderer.JSON(rw, http.StatusOK, rtRepr)
err := json.NewEncoder(rw).Encode(result)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func pagination(request *http.Request, max int) (pageInfo, error) {
perPage, err := getIntParam(request, "per_page", defaultPerPage)
if err != nil {
return pageInfo{}, err
}
page, err := getIntParam(request, "page", defaultPage)
if err != nil {
return pageInfo{}, err
}
startIndex := (page - 1) * perPage
if startIndex != 0 && startIndex >= max {
return pageInfo{}, fmt.Errorf("invalid request: page: %d, per_page: %d", page, perPage)
}
endIndex := startIndex + perPage
if endIndex >= max {
endIndex = max
}
nextPage := 1
if page*perPage < max {
nextPage = page + 1
}
return pageInfo{startIndex: startIndex, endIndex: endIndex, nextPage: nextPage}, nil
}
func getIntParam(request *http.Request, key string, defaultValue int) (int, error) {
raw := request.URL.Query().Get(key)
if raw == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return 0, fmt.Errorf("invalid request: %s: %d", key, value)
}
return value, nil
}
func getProviderName(id string) string {
return strings.SplitN(id, ".", 2)[0]
}

View file

@ -3,9 +3,11 @@ package api
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/containous/mux"
@ -17,6 +19,882 @@ 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 config.RuntimeConfiguration
expected expected
}{
{
desc: "all TCP routers, but no config",
path: "/api/tcp/routers",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/tcprouters-empty.json",
},
},
{
desc: "all TCP routers",
path: "/api/tcp/routers",
conf: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.test": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar.other`)",
TLS: &config.RouterTCPTLSConfig{
Passthrough: false,
},
},
},
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
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: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
"myprovider.baz": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`toto.bar`)",
},
},
"myprovider.test": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
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/myprovider.bar",
conf: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
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/myprovider.foo",
conf: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "one TCP router by id, but no config",
path: "/api/tcp/routers/myprovider.bar",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "all tcp services, but no config",
path: "/api/tcp/services",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/tcpservices-empty.json",
},
},
{
desc: "all tcp services",
path: "/api/tcp/services",
conf: config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.bar": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:2345",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
},
"myprovider.baz": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.2:2345",
},
},
},
},
UsedBy: []string{"myprovider.foo"},
},
},
},
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: config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.bar": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:2345",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
},
"myprovider.baz": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.2:2345",
},
},
},
},
UsedBy: []string{"myprovider.foo"},
},
"myprovider.test": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.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/myprovider.bar",
conf: config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.bar": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:2345",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
},
},
},
expected: expected{
statusCode: http.StatusOK,
jsonFile: "testdata/tcpservice-bar.json",
},
},
{
desc: "one tcp service by id, that does not exist",
path: "/api/tcp/services/myprovider.nono",
conf: config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.bar": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:2345",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
},
},
},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "one tcp service by id, but no config",
path: "/api/tcp/services/myprovider.foo",
conf: config.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
}
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 config.RuntimeConfiguration
expected expected
}{
{
desc: "all routers, but no config",
path: "/api/http/routers",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/routers-empty.json",
},
},
{
desc: "all routers",
path: "/api/http/routers",
conf: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.test": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar.other`)",
Middlewares: []string{"addPrefixTest", "auth"},
},
},
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "anotherprovider.addPrefixTest"},
},
},
},
},
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: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "anotherprovider.addPrefixTest"},
},
},
"myprovider.baz": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`toto.bar`)",
},
},
"myprovider.test": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
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: config.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: config.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: config.RuntimeConfiguration{
Routers: generateHTTPRouters(10),
},
expected: expected{
statusCode: http.StatusBadRequest,
},
},
{
desc: "one router by id",
path: "/api/http/routers/myprovider.bar",
conf: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "anotherprovider.addPrefixTest"},
},
},
},
},
expected: expected{
statusCode: http.StatusOK,
jsonFile: "testdata/router-bar.json",
},
},
{
desc: "one router by id, that does not exist",
path: "/api/http/routers/myprovider.foo",
conf: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "anotherprovider.addPrefixTest"},
},
},
},
},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "one router by id, but no config",
path: "/api/http/routers/myprovider.foo",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "all services, but no config",
path: "/api/http/services",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/services-empty.json",
},
},
{
desc: "all services",
path: "/api/http/services",
conf: config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.bar": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
}
si.UpdateStatus("http://127.0.0.1", "UP")
return si
}(),
"myprovider.baz": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.2",
},
},
},
},
UsedBy: []string{"myprovider.foo"},
}
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: config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.bar": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
}
si.UpdateStatus("http://127.0.0.1", "UP")
return si
}(),
"myprovider.baz": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.2",
},
},
},
},
UsedBy: []string{"myprovider.foo"},
}
si.UpdateStatus("http://127.0.0.2", "UP")
return si
}(),
"myprovider.test": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.3",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
}
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/myprovider.bar",
conf: config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.bar": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
}
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/myprovider.nono",
conf: config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.bar": func() *config.ServiceInfo {
si := &config.ServiceInfo{
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
UsedBy: []string{"myprovider.foo", "myprovider.test"},
}
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/myprovider.foo",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "all middlewares, but no config",
path: "/api/http/middlewares",
conf: config.RuntimeConfiguration{},
expected: expected{
statusCode: http.StatusOK,
nextPage: "1",
jsonFile: "testdata/middlewares-empty.json",
},
},
{
desc: "all middlewares",
path: "/api/http/middlewares",
conf: config.RuntimeConfiguration{
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
"myprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/titi",
},
},
UsedBy: []string{"myprovider.test"},
},
"anotherprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/toto",
},
},
UsedBy: []string{"myprovider.bar"},
},
},
},
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: config.RuntimeConfiguration{
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
"myprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/titi",
},
},
UsedBy: []string{"myprovider.test"},
},
"anotherprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/toto",
},
},
UsedBy: []string{"myprovider.bar"},
},
},
},
expected: expected{
statusCode: http.StatusOK,
nextPage: "3",
jsonFile: "testdata/middlewares-page2.json",
},
},
{
desc: "one middleware by id",
path: "/api/http/middlewares/myprovider.auth",
conf: config.RuntimeConfiguration{
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
"myprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/titi",
},
},
UsedBy: []string{"myprovider.test"},
},
"anotherprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/toto",
},
},
UsedBy: []string{"myprovider.bar"},
},
},
},
expected: expected{
statusCode: http.StatusOK,
jsonFile: "testdata/middleware-auth.json",
},
},
{
desc: "one middleware by id, that does not exist",
path: "/api/http/middlewares/myprovider.foo",
conf: config.RuntimeConfiguration{
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
},
},
expected: expected{
statusCode: http.StatusNotFound,
},
},
{
desc: "one middleware by id, but no config",
path: "/api/http/middlewares/myprovider.foo",
conf: config.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
}
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) {
type expected struct {
statusCode int
@ -130,11 +1008,13 @@ func TestHandler_Configuration(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
// TODO: server status
rtConf := &test.conf
rtConf.PopulateUsedBy()
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
router := mux.NewRouter()
handler.Append(router)
rtConf.PopulateUsedBy()
server := httptest.NewServer(router)
@ -170,3 +1050,17 @@ func TestHandler_Configuration(t *testing.T) {
})
}
}
func generateHTTPRouters(nbRouters int) map[string]*config.RouterInfo {
routers := make(map[string]*config.RouterInfo, nbRouters)
for i := 0; i < nbRouters; i++ {
routers[fmt.Sprintf("myprovider.bar%2d", i)] = &config.RouterInfo{
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar" + strconv.Itoa(i) + "`)",
},
}
}
return routers
}

13
pkg/api/testdata/middleware-auth.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"basicAuth": {
"users": [
"admin:admin"
]
},
"name": "myprovider.auth",
"provider": "myprovider",
"usedBy": [
"myprovider.bar",
"myprovider.test"
]
}

View file

@ -0,0 +1 @@
[]

12
pkg/api/testdata/middlewares-page2.json vendored Normal file
View file

@ -0,0 +1,12 @@
[
{
"addPrefix": {
"prefix": "/titi"
},
"name": "myprovider.addPrefixTest",
"provider": "myprovider",
"usedBy": [
"myprovider.test"
]
}
]

35
pkg/api/testdata/middlewares.json vendored Normal file
View file

@ -0,0 +1,35 @@
[
{
"addPrefix": {
"prefix": "/toto"
},
"name": "anotherprovider.addPrefixTest",
"provider": "anotherprovider",
"usedBy": [
"myprovider.bar"
]
},
{
"addPrefix": {
"prefix": "/titi"
},
"name": "myprovider.addPrefixTest",
"provider": "myprovider",
"usedBy": [
"myprovider.test"
]
},
{
"basicAuth": {
"users": [
"admin:admin"
]
},
"name": "myprovider.auth",
"provider": "myprovider",
"usedBy": [
"myprovider.bar",
"myprovider.test"
]
}
]

13
pkg/api/testdata/router-bar.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"entryPoints": [
"web"
],
"middlewares": [
"auth",
"anotherprovider.addPrefixTest"
],
"name": "myprovider.bar",
"provider": "myprovider",
"rule": "Host(`foo.bar`)",
"service": "myprovider.foo-service"
}

1
pkg/api/testdata/routers-empty.json vendored Normal file
View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,47 @@
[
{
"entryPoints": [
"web"
],
"name": "myprovider.bar14",
"provider": "myprovider",
"rule": "Host(`foo.bar14`)",
"service": "myprovider.foo-service"
},
{
"entryPoints": [
"web"
],
"name": "myprovider.bar15",
"provider": "myprovider",
"rule": "Host(`foo.bar15`)",
"service": "myprovider.foo-service"
},
{
"entryPoints": [
"web"
],
"name": "myprovider.bar16",
"provider": "myprovider",
"rule": "Host(`foo.bar16`)",
"service": "myprovider.foo-service"
},
{
"entryPoints": [
"web"
],
"name": "myprovider.bar17",
"provider": "myprovider",
"rule": "Host(`foo.bar17`)",
"service": "myprovider.foo-service"
},
{
"entryPoints": [
"web"
],
"name": "myprovider.bar18",
"provider": "myprovider",
"rule": "Host(`foo.bar18`)",
"service": "myprovider.foo-service"
}
]

11
pkg/api/testdata/routers-page2.json vendored Normal file
View file

@ -0,0 +1,11 @@
[
{
"entryPoints": [
"web"
],
"name": "myprovider.baz",
"provider": "myprovider",
"rule": "Host(`toto.bar`)",
"service": "myprovider.foo-service"
}
]

28
pkg/api/testdata/routers.json vendored Normal file
View file

@ -0,0 +1,28 @@
[
{
"entryPoints": [
"web"
],
"middlewares": [
"auth",
"anotherprovider.addPrefixTest"
],
"name": "myprovider.bar",
"provider": "myprovider",
"rule": "Host(`foo.bar`)",
"service": "myprovider.foo-service"
},
{
"entryPoints": [
"web"
],
"middlewares": [
"addPrefixTest",
"auth"
],
"name": "myprovider.test",
"provider": "myprovider",
"rule": "Host(`foo.bar.other`)",
"service": "myprovider.foo-service"
}
]

19
pkg/api/testdata/service-bar.json vendored Normal file
View file

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

1
pkg/api/testdata/services-empty.json vendored Normal file
View file

@ -0,0 +1 @@
[]

20
pkg/api/testdata/services-page2.json vendored Normal file
View file

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

39
pkg/api/testdata/services.json vendored Normal file
View file

@ -0,0 +1,39 @@
[
{
"loadbalancer": {
"passHostHeader": false,
"servers": [
{
"url": "http://127.0.0.1"
}
]
},
"name": "myprovider.bar",
"provider": "myprovider",
"serverStatus": {
"http://127.0.0.1": "UP"
},
"usedBy": [
"myprovider.foo",
"myprovider.test"
]
},
{
"loadbalancer": {
"passHostHeader": false,
"servers": [
{
"url": "http://127.0.0.2"
}
]
},
"name": "myprovider.baz",
"provider": "myprovider",
"serverStatus": {
"http://127.0.0.2": "UP"
},
"usedBy": [
"myprovider.foo"
]
}
]

9
pkg/api/testdata/tcprouter-bar.json vendored Normal file
View file

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

View file

@ -0,0 +1 @@
[]

11
pkg/api/testdata/tcprouters-page2.json vendored Normal file
View file

@ -0,0 +1,11 @@
[
{
"entryPoints": [
"web"
],
"name": "myprovider.baz",
"provider": "myprovider",
"rule": "Host(`toto.bar`)",
"service": "myprovider.foo-service"
}
]

23
pkg/api/testdata/tcprouters.json vendored Normal file
View file

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

15
pkg/api/testdata/tcpservice-bar.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"loadbalancer": {
"servers": [
{
"address": "127.0.0.1:2345"
}
]
},
"name": "myprovider.bar",
"provider": "myprovider",
"usedBy": [
"myprovider.foo",
"myprovider.test"
]
}

View file

@ -0,0 +1 @@
[]

16
pkg/api/testdata/tcpservices-page2.json vendored Normal file
View file

@ -0,0 +1,16 @@
[
{
"loadbalancer": {
"servers": [
{
"address": "127.0.0.2:2345"
}
]
},
"name": "myprovider.baz",
"provider": "myprovider",
"usedBy": [
"myprovider.foo"
]
}
]

31
pkg/api/testdata/tcpservices.json vendored Normal file
View file

@ -0,0 +1,31 @@
[
{
"loadbalancer": {
"servers": [
{
"address": "127.0.0.1:2345"
}
]
},
"name": "myprovider.bar",
"provider": "myprovider",
"usedBy": [
"myprovider.foo",
"myprovider.test"
]
},
{
"loadbalancer": {
"servers": [
{
"address": "127.0.0.2:2345"
}
]
},
"name": "myprovider.baz",
"provider": "myprovider",
"usedBy": [
"myprovider.foo"
]
}
]