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

@ -204,16 +204,11 @@ func configureLogging(staticConfiguration *static.Configuration) {
// an explicitly defined log level always has precedence. if none is // an explicitly defined log level always has precedence. if none is
// given and debug mode is disabled, the default is ERROR, and DEBUG // given and debug mode is disabled, the default is ERROR, and DEBUG
// otherwise. // otherwise.
var levelStr string levelStr := "error"
if staticConfiguration.Log != nil { if staticConfiguration.Log != nil && staticConfiguration.Log.Level != "" {
levelStr = strings.ToLower(staticConfiguration.Log.Level) levelStr = strings.ToLower(staticConfiguration.Log.Level)
} }
if levelStr == "" {
levelStr = "error"
if staticConfiguration.Global.Debug {
levelStr = "debug"
}
}
level, err := logrus.ParseLevel(levelStr) level, err := logrus.ParseLevel(levelStr)
if err != nil { if err != nil {
log.WithoutContext().Errorf("Error getting level: %v", err) log.WithoutContext().Errorf("Error getting level: %v", err)

View file

@ -0,0 +1,168 @@
# API
Traefik exposes a number of information through an API handler, such as the configuration of all routers, services, middlewares, etc.
As with all features of Traefik, this handler can be enabled with the [static configuration](../getting-started/configuration-overview.md#the-static-configuration).
## Security
Enabling the API in production is not recommended, because it will expose all configuration elements,
including sensitive data.
In production, it should be at least secured by authentication and authorizations.
A good sane default (non exhaustive) set of recommendations
would be to apply the following protection mechanisms:
* At the application level:
securing with middlewares such as [basic authentication](../middlewares/basicauth.md) or [white listing](../middlewares/ipwhitelist.md).
* At the transport level:
NOT publicly exposing the API's port,
keeping it restricted to internal networks
(as in the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege), applied to networks).
## Configuration
To enable the API handler:
```toml tab="File"
[api]
```
```bash tab="CLI"
--api
```
### `dashboard`
_Optional, Default=true_
Enable the dashboard. More about the dashboard features [here](./dashboard.md).
```toml tab="File"
[api]
dashboard = true
```
```bash tab="CLI"
--api.dashboard
```
### `entrypoint`
_Optional, Default="traefik"_
The entry point that the API handler will be bound to.
The default ("traefik") is an internal entry point (which is always defined).
```toml tab="File"
[api]
entrypoint = "web"
```
```bash tab="CLI"
--api.entrypoint="web"
```
### `middlewares`
_Optional, Default=empty_
The list of [middlewares](../middlewares/overview.md) applied to the API handler.
```toml tab="File"
[api]
middlewares = ["api-auth", "api-prefix"]
```
```bash tab="CLI"
--api.middlewares="api-auth,api-prefix"
```
### `debug`
_Optional, Default=false_
Enable additional endpoints for debugging and profiling, served under `/debug/`.
```toml tab="File"
[api]
debug = true
```
```bash tab="CLI"
--api.debug=true
```
## Endpoints
All the following endpoints must be accessed with a `GET` HTTP request.
| Path | Description |
|--------------------------------|-------------------------------------------------------------------------------------------|
| `/api/http/routers` | Lists all the HTTP routers information. |
| `/api/http/routers/{name}` | Returns the information of the HTTP router specified by `name`. |
| `/api/http/services` | Lists all the HTTP services information. |
| `/api/http/services/{name}` | Returns the information of the HTTP service specified by `name`. |
| `/api/http/middlewares` | Lists all the HTTP middlewares information. |
| `/api/http/middlewares/{name}` | Returns the information of the HTTP middleware specified by `name`. |
| `/api/tcp/routers` | Lists all the TCP routers information. |
| `/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/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. |
| `/debug/pprof/cmdline` | See the [pprof Cmdline](https://golang.org/pkg/net/http/pprof/#Cmdline) Go documentation. |
| `/debug/pprof/profile` | See the [pprof Profile](https://golang.org/pkg/net/http/pprof/#Profile) Go documentation. |
| `/debug/pprof/symbol` | See the [pprof Symbol](https://golang.org/pkg/net/http/pprof/#Symbol) Go documentation. |
| `/debug/pprof/trace` | See the [pprof Trace](https://golang.org/pkg/net/http/pprof/#Trace) Go documentation. |
## Common Configuration Use Cases
### Address / Port
You can define a custom address/port like this:
```toml
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.foo]
address = ":8082"
[entryPoints.bar]
address = ":8083"
[ping]
entryPoint = "foo"
[api]
entryPoint = "bar"
```
In the above example, you would access a service at /foo, an api endpoint, or the health-check as follows:
* Service: `http://hostname:80/foo`
* API: `http://hostname:8083/api/http/routers`
* Ping URL: `http://hostname:8082/ping`
### Authentication
To restrict access to the API handler, one can add authentication with the [basic auth middleware](../middlewares/basicauth.md).
```toml
[api]
middlewares=["api-auth"]
```
```toml
[http.middlewares]
[http.middlewares.api-auth.basicauth]
users = [
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
]
```

View file

@ -1,15 +0,0 @@
# The Debug Mode
Getting More Information (Not For Production)
{: .subtitle }
The debug mode will make Traefik be _extremely_ verbose in its logs, and is NOT intended for production purposes.
## Configuration Example
??? example "TOML -- Enabling the Debug Mode"
```toml
[Global]
debug = true
```

View file

@ -1,3 +1,4 @@
--accesslog (Default: "false") --accesslog (Default: "false")
Access log settings. Access log settings.
@ -95,8 +96,11 @@
--api.dashboard (Default: "true") --api.dashboard (Default: "true")
Activate dashboard. Activate dashboard.
--api.debug (Default: "false")
Enable additional endpoints for debugging and profiling.
--api.entrypoint (Default: "traefik") --api.entrypoint (Default: "traefik")
EntryPoint. The entry point that the API handler will be bound to.
--api.middlewares (Default: "") --api.middlewares (Default: "")
Middleware list. Middleware list.
@ -153,9 +157,6 @@
--global.checknewversion (Default: "true") --global.checknewversion (Default: "true")
Periodically check if a new version has been released. Periodically check if a new version has been released.
--global.debug (Default: "false")
Enable debug mode.
--global.sendanonymoususage --global.sendanonymoususage
Periodically send anonymous usage statistics. If the option is not specified, it Periodically send anonymous usage statistics. If the option is not specified, it
will be enabled by default. will be enabled by default.

View file

@ -93,8 +93,11 @@ Enable api/dashboard. (Default: ```false```)
`TRAEFIK_API_DASHBOARD`: `TRAEFIK_API_DASHBOARD`:
Activate dashboard. (Default: ```true```) Activate dashboard. (Default: ```true```)
`TRAEFIK_API_DEBUG`:
Enable additional endpoints for debugging and profiling. (Default: ```false```)
`TRAEFIK_API_ENTRYPOINT`: `TRAEFIK_API_ENTRYPOINT`:
EntryPoint. (Default: ```traefik```) The entry point that the API handler will be bound to. (Default: ```traefik```)
`TRAEFIK_API_MIDDLEWARES`: `TRAEFIK_API_MIDDLEWARES`:
Middleware list. Middleware list.
@ -147,9 +150,6 @@ WriteTimeout is the maximum duration before timing out writes of the response. I
`TRAEFIK_GLOBAL_CHECKNEWVERSION`: `TRAEFIK_GLOBAL_CHECKNEWVERSION`:
Periodically check if a new version has been released. (Default: ```false```) Periodically check if a new version has been released. (Default: ```false```)
`TRAEFIK_GLOBAL_DEBUG`:
Enable debug mode. (Default: ```false```)
`TRAEFIK_GLOBAL_SENDANONYMOUSUSAGE`: `TRAEFIK_GLOBAL_SENDANONYMOUSUSAGE`:
Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default. Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default.

View file

@ -1,5 +1,4 @@
[Global] [Global]
Debug = true
CheckNewVersion = true CheckNewVersion = true
SendAnonymousUsage = true SendAnonymousUsage = true

View file

@ -113,8 +113,8 @@ nav:
- 'Operations': - 'Operations':
- 'CLI': 'operations/cli.md' - 'CLI': 'operations/cli.md'
- 'Dashboard' : 'operations/dashboard.md' - 'Dashboard' : 'operations/dashboard.md'
- 'API': 'operations/api.md'
- 'Ping': 'operations/ping.md' - 'Ping': 'operations/ping.md'
- 'Debug Mode': 'operations/debug-mode.md'
- 'Observability': - 'Observability':
- 'Logs': 'observability/logs.md' - 'Logs': 'observability/logs.md'
- 'Access Logs': 'observability/access-logs.md' - 'Access Logs': 'observability/access-logs.md'

View file

@ -161,7 +161,7 @@ func (s *SimpleSuite) TestApiOnSameEntryPoint(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--api.entryPoint=http", "--global.debug", "--providers.docker") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--api.entryPoint=http", "--log.level=DEBUG", "--providers.docker")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -241,7 +241,7 @@ func (s *SimpleSuite) TestDefaultEntrypointHTTP(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--global.debug", "--providers.docker", "--api") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--log.level=DEBUG", "--providers.docker", "--api")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -259,7 +259,7 @@ func (s *SimpleSuite) TestWithUnexistingEntrypoint(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--global.debug", "--providers.docker", "--api") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--log.level=DEBUG", "--providers.docker", "--api")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()
@ -277,7 +277,7 @@ func (s *SimpleSuite) TestMetricsPrometheusDefaultEntrypoint(c *check.C) {
s.createComposeProject(c, "base") s.createComposeProject(c, "base")
s.composeProject.Start(c) s.composeProject.Start(c)
cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--api", "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0", "--providers.docker", "--global.debug") cmd, output := s.traefikCmd("--entryPoints.http.Address=:8000", "--api", "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0", "--providers.docker", "--log.level=DEBUG")
defer output(c) defer output(c)
err := cmd.Start() err := cmd.Start()

View file

@ -49,7 +49,7 @@ func (s *WebsocketSuite) TestBase(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -99,7 +99,7 @@ func (s *WebsocketSuite) TestWrongOrigin(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -149,7 +149,7 @@ func (s *WebsocketSuite) TestOrigin(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -210,7 +210,7 @@ func (s *WebsocketSuite) TestWrongOriginIgnoredByServer(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -268,7 +268,7 @@ func (s *WebsocketSuite) TestSSLTermination(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -331,7 +331,7 @@ func (s *WebsocketSuite) TestBasicAuth(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -375,7 +375,7 @@ func (s *WebsocketSuite) TestSpecificResponseFromBackend(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -421,7 +421,7 @@ func (s *WebsocketSuite) TestURLWithURLEncodedChar(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -476,7 +476,7 @@ func (s *WebsocketSuite) TestSSLhttp2(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug", "--accesslog") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG", "--accesslog")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()
@ -535,7 +535,7 @@ func (s *WebsocketSuite) TestHeaderAreForwared(c *check.C) {
}) })
defer os.Remove(file) defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file), "--global.debug") cmd, display := s.traefikCmd(withConfigFile(file), "--log.level=DEBUG")
defer display(c) defer display(c)
err := cmd.Start() err := cmd.Start()

View file

@ -28,7 +28,6 @@ func TestDo_globalConfiguration(t *testing.T) {
sendAnonymousUsage := true sendAnonymousUsage := true
config.Global = &static.Global{ config.Global = &static.Global{
Debug: true,
CheckNewVersion: true, CheckNewVersion: true,
SendAnonymousUsage: &sendAnonymousUsage, SendAnonymousUsage: &sendAnonymousUsage,
} }

View file

@ -1,8 +1,12 @@
package api package api
import ( import (
"io" "encoding/json"
"fmt"
"net/http" "net/http"
"sort"
"strconv"
"strings"
"github.com/containous/mux" "github.com/containous/mux"
"github.com/containous/traefik/pkg/config" "github.com/containous/traefik/pkg/config"
@ -11,14 +15,14 @@ import (
"github.com/containous/traefik/pkg/types" "github.com/containous/traefik/pkg/types"
"github.com/containous/traefik/pkg/version" "github.com/containous/traefik/pkg/version"
assetfs "github.com/elazarl/go-bindata-assetfs" 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 { const nextPageHeader = "X-Next-Page"
JSON(w io.Writer, status int, v interface{}) error
}
type serviceInfoRepresentation struct { type serviceInfoRepresentation struct {
*config.ServiceInfo *config.ServiceInfo
@ -34,6 +38,43 @@ type RunTimeRepresentation struct {
TCPServices map[string]*config.TCPServiceInfo `json:"tcpServices,omitempty"` 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. // Handler serves the configuration and status of Traefik on API endpoints.
type Handler struct { type Handler struct {
dashboard bool dashboard bool
@ -59,7 +100,7 @@ func New(staticConfig static.Configuration, runtimeConfig *config.RuntimeConfigu
statistics: staticConfig.API.Statistics, statistics: staticConfig.API.Statistics,
dashboardAssets: staticConfig.API.DashboardAssets, dashboardAssets: staticConfig.API.DashboardAssets,
runtimeConfiguration: rConfig, runtimeConfiguration: rConfig,
debug: staticConfig.Global.Debug, debug: staticConfig.API.Debug,
} }
} }
@ -71,6 +112,18 @@ func (h Handler) Append(router *mux.Router) {
router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration) 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 // FIXME stats
// health route // health route
// router.Methods(http.MethodGet).Path("/health").HandlerFunc(p.getHealthHandler) // router.Methods(http.MethodGet).Path("/health").HandlerFunc(p.getHealthHandler)
@ -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) { func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) {
siRepr := make(map[string]*serviceInfoRepresentation, len(h.runtimeConfiguration.Services)) siRepr := make(map[string]*serviceInfoRepresentation, len(h.runtimeConfiguration.Services))
for k, v := range 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, Routers: h.runtimeConfiguration.Routers,
Middlewares: h.runtimeConfiguration.Middlewares, Middlewares: h.runtimeConfiguration.Middlewares,
Services: siRepr, Services: siRepr,
@ -99,9 +414,55 @@ func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.R
TCPServices: h.runtimeConfiguration.TCPServices, TCPServices: h.runtimeConfiguration.TCPServices,
} }
err := templateRenderer.JSON(rw, http.StatusOK, rtRepr) err := json.NewEncoder(rw).Encode(result)
if err != nil { if err != nil {
log.FromContext(request.Context()).Error(err) log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError) 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 ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"testing" "testing"
"github.com/containous/mux" "github.com/containous/mux"
@ -17,6 +19,882 @@ import (
var updateExpected = flag.Bool("update_expected", false, "Update expected files in testdata") 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) { func TestHandler_Configuration(t *testing.T) {
type expected struct { type expected struct {
statusCode int statusCode int
@ -130,11 +1008,13 @@ func TestHandler_Configuration(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
// TODO: server status
rtConf := &test.conf rtConf := &test.conf
rtConf.PopulateUsedBy()
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf) handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
router := mux.NewRouter() router := mux.NewRouter()
handler.Append(router) handler.Append(router)
rtConf.PopulateUsedBy()
server := httptest.NewServer(router) 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"
]
}
]

View file

@ -66,7 +66,6 @@ type Configuration struct {
// Global holds the global configuration. // Global holds the global configuration.
type Global struct { type Global struct {
Debug bool `description:"Enable debug mode." export:"true"`
CheckNewVersion bool `description:"Periodically check if a new version has been released." export:"true"` CheckNewVersion bool `description:"Periodically check if a new version has been released." export:"true"`
SendAnonymousUsage *bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default." export:"true"` SendAnonymousUsage *bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default." export:"true"`
} }
@ -81,8 +80,9 @@ type ServersTransport struct {
// API holds the API configuration // API holds the API configuration
type API struct { type API struct {
EntryPoint string `description:"EntryPoint." export:"true"` EntryPoint string `description:"The entry point that the API handler will be bound to." export:"true"`
Dashboard bool `description:"Activate dashboard." export:"true"` Dashboard bool `description:"Activate dashboard." export:"true"`
Debug bool `description:"Enable additional endpoints for debugging and profiling." export:"true"`
Statistics *types.Statistics `description:"Enable more detailed statistics." export:"true" label:"allowEmpty"` Statistics *types.Statistics `description:"Enable more detailed statistics." export:"true" label:"allowEmpty"`
Middlewares []string `description:"Middleware list." export:"true"` Middlewares []string `description:"Middleware list." export:"true"`
DashboardAssets *assetfs.AssetFS `json:"-" label:"-"` DashboardAssets *assetfs.AssetFS `json:"-" label:"-"`