Split Web into API/Dashboard, ping, metric and Rest Provider

This commit is contained in:
SALLEYRON Julien 2017-11-09 16:12:04 +01:00 committed by Traefiker
parent 384488ac02
commit 27d1b46835
24 changed files with 1252 additions and 377 deletions

23
api/dashboard.go Normal file
View file

@ -0,0 +1,23 @@
package api
import (
"net/http"
"github.com/containous/mux"
"github.com/containous/traefik/autogen"
"github.com/elazarl/go-bindata-assetfs"
)
// DashboardHandler expose dashboard routes
type DashboardHandler struct{}
// AddRoutes add dashboard routes on a router
func (g DashboardHandler) AddRoutes(router *mux.Router) {
// Expose dashboard
router.Methods("GET").Path("/").HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
http.Redirect(response, request, "/dashboard/", 302)
})
router.Methods("GET").PathPrefix("/dashboard/").
Handler(http.StripPrefix("/dashboard/", http.FileServer(&assetfs.AssetFS{Asset: autogen.Asset, AssetInfo: autogen.AssetInfo, AssetDir: autogen.AssetDir, Prefix: "static"})))
}

39
api/debug.go Normal file
View file

@ -0,0 +1,39 @@
package api
import (
"expvar"
"fmt"
"net/http"
"runtime"
"github.com/containous/mux"
)
func init() {
expvar.Publish("Goroutines", expvar.Func(goroutines))
}
func goroutines() interface{} {
return runtime.NumGoroutine()
}
// DebugHandler expose debug routes
type DebugHandler struct{}
// AddRoutes add debug routes on a router
func (g DebugHandler) AddRoutes(router *mux.Router) {
router.Methods("GET").Path("/debug/vars").
HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprint(w, "{\n")
first := true
expvar.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprint(w, ",\n")
}
first = false
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
})
fmt.Fprint(w, "\n}\n")
})
}

250
api/handler.go Normal file
View file

@ -0,0 +1,250 @@
package api
import (
"net/http"
"github.com/containous/mux"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"github.com/containous/traefik/version"
thoas_stats "github.com/thoas/stats"
"github.com/unrolled/render"
)
// Handler expose api routes
type Handler struct {
EntryPoint string `description:"EntryPoint" export:"true"`
Dashboard bool `description:"Activate dashboard" export:"true"`
Debug bool `export:"true"`
CurrentConfigurations *safe.Safe
Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"`
Stats *thoas_stats.Stats
StatsRecorder *middlewares.StatsRecorder
}
var (
templatesRenderer = render.New(render.Options{
Directory: "nowhere",
})
)
// AddRoutes add api routes on a router
func (p Handler) AddRoutes(router *mux.Router) {
if p.Debug {
DebugHandler{}.AddRoutes(router)
}
router.Methods("GET").Path("/api").HandlerFunc(p.getConfigHandler)
router.Methods("GET").Path("/api/providers").HandlerFunc(p.getConfigHandler)
router.Methods("GET").Path("/api/providers/{provider}").HandlerFunc(p.getProviderHandler)
router.Methods("GET").Path("/api/providers/{provider}/backends").HandlerFunc(p.getBackendsHandler)
router.Methods("GET").Path("/api/providers/{provider}/backends/{backend}").HandlerFunc(p.getBackendHandler)
router.Methods("GET").Path("/api/providers/{provider}/backends/{backend}/servers").HandlerFunc(p.getServersHandler)
router.Methods("GET").Path("/api/providers/{provider}/backends/{backend}/servers/{server}").HandlerFunc(p.getServerHandler)
router.Methods("GET").Path("/api/providers/{provider}/frontends").HandlerFunc(p.getFrontendsHandler)
router.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}").HandlerFunc(p.getFrontendHandler)
router.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}/routes").HandlerFunc(p.getRoutesHandler)
router.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}/routes/{route}").HandlerFunc(p.getRouteHandler)
// health route
router.Methods("GET").Path("/health").HandlerFunc(p.getHealthHandler)
version.Handler{}.AddRoutes(router)
if p.Dashboard {
DashboardHandler{}.AddRoutes(router)
}
}
func getProviderIDFromVars(vars map[string]string) string {
providerID := vars["provider"]
// TODO: Deprecated
if providerID == "rest" {
providerID = "web"
}
return providerID
}
func (p Handler) getConfigHandler(response http.ResponseWriter, request *http.Request) {
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
err := templatesRenderer.JSON(response, http.StatusOK, currentConfigurations)
if err != nil {
log.Error(err)
}
}
func (p Handler) getProviderHandler(response http.ResponseWriter, request *http.Request) {
providerID := getProviderIDFromVars(mux.Vars(request))
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, provider)
if err != nil {
log.Error(err)
}
} else {
http.NotFound(response, request)
}
}
func (p Handler) getBackendsHandler(response http.ResponseWriter, request *http.Request) {
providerID := getProviderIDFromVars(mux.Vars(request))
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, provider.Backends)
if err != nil {
log.Error(err)
}
} else {
http.NotFound(response, request)
}
}
func (p Handler) getBackendHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := getProviderIDFromVars(vars)
backendID := vars["backend"]
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, backend)
if err != nil {
log.Error(err)
}
return
}
}
http.NotFound(response, request)
}
func (p Handler) getServersHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := getProviderIDFromVars(vars)
backendID := vars["backend"]
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, backend.Servers)
if err != nil {
log.Error(err)
}
return
}
}
http.NotFound(response, request)
}
func (p Handler) getServerHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := getProviderIDFromVars(vars)
backendID := vars["backend"]
serverID := vars["server"]
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
if server, ok := backend.Servers[serverID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, server)
if err != nil {
log.Error(err)
}
return
}
}
}
http.NotFound(response, request)
}
func (p Handler) getFrontendsHandler(response http.ResponseWriter, request *http.Request) {
providerID := getProviderIDFromVars(mux.Vars(request))
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, provider.Frontends)
if err != nil {
log.Error(err)
}
} else {
http.NotFound(response, request)
}
}
func (p Handler) getFrontendHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := getProviderIDFromVars(vars)
frontendID := vars["frontend"]
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, frontend)
if err != nil {
log.Error(err)
}
return
}
}
http.NotFound(response, request)
}
func (p Handler) getRoutesHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := getProviderIDFromVars(vars)
frontendID := vars["frontend"]
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, frontend.Routes)
if err != nil {
log.Error(err)
}
return
}
}
http.NotFound(response, request)
}
func (p Handler) getRouteHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := getProviderIDFromVars(vars)
frontendID := vars["frontend"]
routeID := vars["route"]
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
if route, ok := frontend.Routes[routeID]; ok {
err := templatesRenderer.JSON(response, http.StatusOK, route)
if err != nil {
log.Error(err)
}
return
}
}
}
http.NotFound(response, request)
}
// healthResponse combines data returned by thoas/stats with statistics (if
// they are enabled).
type healthResponse struct {
*thoas_stats.Data
*middlewares.Stats
}
func (p *Handler) getHealthHandler(response http.ResponseWriter, request *http.Request) {
health := &healthResponse{Data: p.Stats.Data()}
if p.StatsRecorder != nil {
health.Stats = p.StatsRecorder.Data()
}
err := templatesRenderer.JSON(response, http.StatusOK, health)
if err != nil {
log.Error(err)
}
}

View file

@ -8,7 +8,6 @@ import (
"github.com/containous/flaeg"
"github.com/containous/traefik/acme"
"github.com/containous/traefik/configuration"
"github.com/containous/traefik/middlewares"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/boltdb"
"github.com/containous/traefik/provider/consul"
@ -23,12 +22,9 @@ import (
"github.com/containous/traefik/provider/marathon"
"github.com/containous/traefik/provider/mesos"
"github.com/containous/traefik/provider/rancher"
"github.com/containous/traefik/provider/web"
"github.com/containous/traefik/provider/zk"
"github.com/containous/traefik/safe"
traefikTls "github.com/containous/traefik/tls"
"github.com/containous/traefik/types"
thoas_stats "github.com/thoas/stats"
)
func TestDo_globalConfiguration(t *testing.T) {
@ -247,7 +243,7 @@ func TestDo_globalConfiguration(t *testing.T) {
},
Directory: "file Directory",
}
config.Web = &web.Provider{
config.Web = &configuration.WebCompatibility{
Address: "web Address",
CertFile: "web CertFile",
KeyFile: "web KeyFile",
@ -290,15 +286,6 @@ func TestDo_globalConfiguration(t *testing.T) {
},
},
Debug: true,
CurrentConfigurations: &safe.Safe{},
Stats: &thoas_stats.Stats{
Uptime: time.Now(),
Pid: 666,
ResponseCounts: map[string]int{"foo": 1, "fii": 2, "fuu": 3},
TotalResponseCounts: map[string]int{"foo": 1, "fii": 2, "fuu": 3},
TotalResponseTime: time.Now(),
},
StatsRecorder: &middlewares.StatsRecorder{},
}
config.Marathon = &marathon.Provider{
BaseProvider: provider.BaseProvider{

View file

@ -4,8 +4,10 @@ import (
"time"
"github.com/containous/flaeg"
"github.com/containous/traefik/api"
"github.com/containous/traefik/configuration"
"github.com/containous/traefik/middlewares/accesslog"
"github.com/containous/traefik/ping"
"github.com/containous/traefik/provider/boltdb"
"github.com/containous/traefik/provider/consul"
"github.com/containous/traefik/provider/docker"
@ -18,7 +20,7 @@ import (
"github.com/containous/traefik/provider/marathon"
"github.com/containous/traefik/provider/mesos"
"github.com/containous/traefik/provider/rancher"
"github.com/containous/traefik/provider/web"
"github.com/containous/traefik/provider/rest"
"github.com/containous/traefik/provider/zk"
"github.com/containous/traefik/types"
)
@ -43,14 +45,18 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultFile.Watch = true
defaultFile.Filename = "" //needs equivalent to viper.ConfigFileUsed()
// default Web
var defaultWeb web.Provider
// default Rest
var defaultRest rest.Provider
defaultRest.EntryPoint = configuration.DefaultInternalEntryPointName
// TODO: Deprecated - Web provider, use REST provider instead
var defaultWeb configuration.WebCompatibility
defaultWeb.Address = ":8080"
defaultWeb.Statistics = &types.Statistics{
RecentErrors: 10,
}
// default Metrics
// TODO: Deprecated - default Metrics
defaultWeb.Metrics = &types.Metrics{
Prometheus: &types.Prometheus{
Buckets: types.Buckets{0.1, 0.3, 1.2, 5},
@ -157,6 +163,11 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
var defaultEureka eureka.Provider
defaultEureka.Delay = "30s"
// default Ping
var defaultPing = ping.Handler{
EntryPoint: "traefik",
}
// default TraefikLog
defaultTraefikLog := types.TraefikLog{
Format: "common",
@ -189,10 +200,40 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
GraceTimeOut: flaeg.Duration(configuration.DefaultGraceTimeout),
}
// default ApiConfiguration
defaultAPI := api.Handler{
EntryPoint: "traefik",
Dashboard: true,
}
defaultAPI.Statistics = &types.Statistics{
RecentErrors: 10,
}
// default Metrics
defaultMetrics := types.Metrics{
Prometheus: &types.Prometheus{
Buckets: types.Buckets{0.1, 0.3, 1.2, 5},
EntryPoint: "traefik",
},
Datadog: &types.Datadog{
Address: "localhost:8125",
PushInterval: "10s",
},
StatsD: &types.Statsd{
Address: "localhost:8125",
PushInterval: "10s",
},
InfluxDB: &types.InfluxDB{
Address: "localhost:8089",
PushInterval: "10s",
},
}
defaultConfiguration := configuration.GlobalConfiguration{
Docker: &defaultDocker,
File: &defaultFile,
Web: &defaultWeb,
Rest: &defaultRest,
Marathon: &defaultMarathon,
Consul: &defaultConsul,
ConsulCatalog: &defaultConsulCatalog,
@ -212,6 +253,9 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
TraefikLog: &defaultTraefikLog,
AccessLog: &defaultAccessLog,
LifeCycle: &defaultLifeycle,
Ping: &defaultPing,
API: &defaultAPI,
Metrics: &defaultMetrics,
}
return &TraefikConfiguration{

View file

@ -102,19 +102,25 @@ Complete documentation is available at https://traefik.io`,
healthCheckCmd := &flaeg.Command{
Name: "healthcheck",
Description: `Calls traefik /ping to check health (web provider must be enabled)`,
Description: `Calls traefik /ping to check health (ping must be enabled)`,
Config: traefikConfiguration,
DefaultPointersConfig: traefikPointersConfiguration,
Run: func() error {
traefikConfiguration.GlobalConfiguration.SetEffectiveConfiguration(traefikConfiguration.ConfigFile)
if traefikConfiguration.Web == nil {
fmt.Println("Please enable the web provider to use healtcheck.")
if traefikConfiguration.Ping == nil {
fmt.Println("Please enable `ping` to use healtcheck.")
os.Exit(1)
}
pingEntryPoint, ok := traefikConfiguration.EntryPoints[traefikConfiguration.Ping.EntryPoint]
if !ok {
pingEntryPoint = &configuration.EntryPoint{Address: ":8080"}
}
client := &http.Client{Timeout: 5 * time.Second}
protocol := "http"
if len(traefikConfiguration.Web.CertFile) > 0 {
if pingEntryPoint.TLS != nil {
protocol = "https"
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
@ -122,9 +128,9 @@ Complete documentation is available at https://traefik.io`,
client.Transport = tr
}
resp, err := client.Head(protocol + "://" + traefikConfiguration.Web.Address + traefikConfiguration.Web.Path + "ping")
if err != nil {
fmt.Printf("Error calling healthcheck: %s\n", err)
resp, errPing := client.Head(protocol + "://" + pingEntryPoint.Address + traefikConfiguration.Web.Path + "ping")
if errPing != nil {
fmt.Printf("Error calling healthcheck: %s\n", errPing)
os.Exit(1)
}
if resp.StatusCode != http.StatusOK {

View file

@ -7,7 +7,9 @@ import (
"github.com/containous/flaeg"
"github.com/containous/traefik/acme"
"github.com/containous/traefik/api"
"github.com/containous/traefik/log"
"github.com/containous/traefik/ping"
"github.com/containous/traefik/provider/boltdb"
"github.com/containous/traefik/provider/consul"
"github.com/containous/traefik/provider/docker"
@ -20,13 +22,16 @@ import (
"github.com/containous/traefik/provider/marathon"
"github.com/containous/traefik/provider/mesos"
"github.com/containous/traefik/provider/rancher"
"github.com/containous/traefik/provider/web"
"github.com/containous/traefik/provider/rest"
"github.com/containous/traefik/provider/zk"
"github.com/containous/traefik/tls"
"github.com/containous/traefik/types"
)
const (
// DefaultInternalEntryPointName the name of the default internal entry point
DefaultInternalEntryPointName = "traefik"
// DefaultHealthCheckInterval is the default health check interval.
DefaultHealthCheckInterval = 30 * time.Second
@ -67,9 +72,9 @@ type GlobalConfiguration struct {
HealthCheck *HealthCheckConfig `description:"Health check parameters" export:"true"`
RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance" export:"true"`
ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers" export:"true"`
Web *WebCompatibility `description:"(Deprecated) Enable Web backend with default settings" export:"true"` // Deprecated
Docker *docker.Provider `description:"Enable Docker backend with default settings" export:"true"`
File *file.Provider `description:"Enable File backend with default settings" export:"true"`
Web *web.Provider `description:"Enable Web backend with default settings" export:"true"`
Marathon *marathon.Provider `description:"Enable Marathon backend with default settings" export:"true"`
Consul *consul.Provider `description:"Enable Consul backend with default settings" export:"true"`
ConsulCatalog *consul.CatalogProvider `description:"Enable Consul catalog backend with default settings" export:"true"`
@ -82,6 +87,70 @@ type GlobalConfiguration struct {
ECS *ecs.Provider `description:"Enable ECS backend with default settings" export:"true"`
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings" export:"true"`
DynamoDB *dynamodb.Provider `description:"Enable DynamoDB backend with default settings" export:"true"`
Rest *rest.Provider `description:"Enable Rest backend with default settings" export:"true"`
API *api.Handler `description:"Enable api/dashboard" export:"true"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"`
Ping *ping.Handler `description:"Enable ping" export:"true"`
}
// WebCompatibility is a configuration to handle compatibility with deprecated web provider options
type WebCompatibility struct {
Address string `description:"Web administration port" export:"true"`
CertFile string `description:"SSL certificate" export:"true"`
KeyFile string `description:"SSL certificate" export:"true"`
ReadOnly bool `description:"Enable read only API" export:"true"`
Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"`
Path string `description:"Root path for dashboard and API" export:"true"`
Auth *types.Auth `export:"true"`
Debug bool `export:"true"`
}
func (gc *GlobalConfiguration) handleWebDeprecation() {
if gc.Web != nil {
log.Warn("web provider configuration is deprecated, you should use these options : api, rest provider, ping and metrics")
if gc.API != nil || gc.Metrics != nil || gc.Ping != nil || gc.Rest != nil {
log.Warn("web option is ignored if you use it with one of these options : api, rest provider, ping or metrics")
return
}
gc.EntryPoints[DefaultInternalEntryPointName] = &EntryPoint{
Address: gc.Web.Address,
Auth: gc.Web.Auth,
}
if gc.Web.CertFile != "" {
gc.EntryPoints[DefaultInternalEntryPointName].TLS = &tls.TLS{
Certificates: []tls.Certificate{
{
CertFile: tls.FileOrContent(gc.Web.CertFile),
KeyFile: tls.FileOrContent(gc.Web.KeyFile),
},
},
}
}
if gc.API == nil {
gc.API = &api.Handler{
EntryPoint: DefaultInternalEntryPointName,
Statistics: gc.Web.Statistics,
Dashboard: true,
}
}
if gc.Ping == nil {
gc.Ping = &ping.Handler{
EntryPoint: DefaultInternalEntryPointName,
}
}
if gc.Metrics == nil {
gc.Metrics = gc.Web.Metrics
}
if !gc.Debug {
gc.Debug = gc.Web.Debug
}
}
}
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
@ -95,6 +164,17 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
gc.DefaultEntryPoints = []string{"http"}
}
gc.handleWebDeprecation()
if (gc.API != nil && gc.API.EntryPoint == DefaultInternalEntryPointName) ||
(gc.Ping != nil && gc.Ping.EntryPoint == DefaultInternalEntryPointName) ||
(gc.Metrics != nil && gc.Metrics.Prometheus != nil && gc.Metrics.Prometheus.EntryPoint == DefaultInternalEntryPointName) ||
(gc.Rest != nil && gc.Rest.EntryPoint == DefaultInternalEntryPointName) {
if _, ok := gc.EntryPoints[DefaultInternalEntryPointName]; !ok {
gc.EntryPoints[DefaultInternalEntryPointName] = &EntryPoint{Address: ":8080"}
}
}
// ForwardedHeaders must be remove in the next breaking version
for entryPointName := range gc.EntryPoints {
entryPoint := gc.EntryPoints[entryPointName]
@ -136,6 +216,14 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
}
}
if gc.API != nil {
gc.API.Debug = gc.Debug
}
if gc.Debug {
gc.LogLevel = "DEBUG"
}
if gc.Web != nil && (gc.Web.Path == "" || !strings.HasSuffix(gc.Web.Path, "/")) {
gc.Web.Path += "/"
}

203
docs/configuration/api.md Normal file
View file

@ -0,0 +1,203 @@
# API Definition
```toml
# API definition
[api]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
entryPoint = "traefik"
# Enabled Dashboard
#
# Optional
# Default: true
#
dashboard = true
# Enabled debug mode
#
# Optional
# Default: false
#
debug = true
```
## Web UI
![Web UI Providers](/img/web.frontend.png)
![Web UI Health](/img/traefik-health.png)
## API
| Path | Method | Description |
|-----------------------------------------------------------------|------------------|-------------------------------------------|
| `/` | `GET` | Provides a simple HTML frontend of Træfik |
| `/health` | `GET` | json health metrics |
| `/api` | `GET` | Configuration for all providers |
| `/api/providers` | `GET` | Providers |
| `/api/providers/{provider}` | `GET`, `PUT` | Get or update provider |
| `/api/providers/{provider}/backends` | `GET` | List backends |
| `/api/providers/{provider}/backends/{backend}` | `GET` | Get backend |
| `/api/providers/{provider}/backends/{backend}/servers` | `GET` | List servers in backend |
| `/api/providers/{provider}/backends/{backend}/servers/{server}` | `GET` | Get a server in a backend |
| `/api/providers/{provider}/frontends` | `GET` | List frontends |
| `/api/providers/{provider}/frontends/{frontend}` | `GET` | Get a frontend |
| `/api/providers/{provider}/frontends/{frontend}/routes` | `GET` | List routes in a frontend |
| `/api/providers/{provider}/frontends/{frontend}/routes/{route}` | `GET` | Get a route in a frontend |
!!! warning
For compatibility reason, when you activate the rest provider, you can use `web` or `rest` as `provider` value.
But be careful, in the configuration for all providers the key is still `web`.
### Provider configurations
```shell
curl -s "http://localhost:8080/api" | jq .
```
```json
{
"file": {
"frontends": {
"frontend2": {
"routes": {
"test_2": {
"rule": "Path:/test"
}
},
"backend": "backend1"
},
"frontend1": {
"routes": {
"test_1": {
"rule": "Host:test.localhost"
}
},
"backend": "backend2"
}
},
"backends": {
"backend2": {
"loadBalancer": {
"method": "drr"
},
"servers": {
"server2": {
"weight": 2,
"URL": "http://172.17.0.5:80"
},
"server1": {
"weight": 1,
"url": "http://172.17.0.4:80"
}
}
},
"backend1": {
"loadBalancer": {
"method": "wrr"
},
"circuitBreaker": {
"expression": "NetworkErrorRatio() > 0.5"
},
"servers": {
"server2": {
"weight": 1,
"url": "http://172.17.0.3:80"
},
"server1": {
"weight": 10,
"url": "http://172.17.0.2:80"
}
}
}
}
}
}
```
### Health
```shell
curl -s "http://localhost:8080/health" | jq .
```
```json
{
// Træfik PID
"pid": 2458,
// Træfik server uptime (formated time)
"uptime": "39m6.885931127s",
// Træfik server uptime in seconds
"uptime_sec": 2346.885931127,
// current server date
"time": "2015-10-07 18:32:24.362238909 +0200 CEST",
// current server date in seconds
"unixtime": 1444235544,
// count HTTP response status code in realtime
"status_code_count": {
"502": 1
},
// count HTTP response status code since Træfik started
"total_status_code_count": {
"200": 7,
"404": 21,
"502": 13
},
// count HTTP response
"count": 1,
// count HTTP response
"total_count": 41,
// sum of all response time (formated time)
"total_response_time": "35.456865605s",
// sum of all response time in seconds
"total_response_time_sec": 35.456865605,
// average response time (formated time)
"average_response_time": "864.8016ms",
// average response time in seconds
"average_response_time_sec": 0.8648016000000001,
// request statistics [requires --web.statistics to be set]
// ten most recent requests with 4xx and 5xx status codes
"recent_errors": [
{
// status code
"status_code": 500,
// description of status code
"status": "Internal Server Error",
// request HTTP method
"method": "GET",
// request hostname
"host": "localhost",
// request path
"path": "/path",
// RFC 3339 formatted date/time
"time": "2016-10-21T16:59:15.418495872-07:00"
}
]
}
```
## Metrics
You can enable Traefik to export internal metrics to different monitoring systems.
```toml
[api]
# ...
# Enable more detailed statistics.
[api.statistics]
# Number of recent errors logged.
#
# Default: 10
#
recentErrors = 10
# ...
```
| Path | Method | Description |
|------------|---------------|-------------------------|
| `/metrics` | `GET` | Export internal metrics |

View file

@ -0,0 +1,91 @@
# Rest Backend
Træfik can be configured:
- using a RESTful api.
## Configuration
```toml
# Enable rest backend.
[rest]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
entryPoint = "traefik"
```
## API
| Path | Method | Description |
|------------------------------|--------|-----------------|
| `/api/providers/web` | `PUT` | update provider |
| `/api/providers/rest` | `PUT` | update provider |
!!! warning
For compatibility reason, when you activate the rest provider, you can use `web` or `rest` as `provider` value.
```shell
curl -XPUT @file "http://localhost:8080/api"
```
with `@file`
```json
{
"frontends": {
"frontend2": {
"routes": {
"test_2": {
"rule": "Path:/test"
}
},
"backend": "backend1"
},
"frontend1": {
"routes": {
"test_1": {
"rule": "Host:test.localhost"
}
},
"backend": "backend2"
}
},
"backends": {
"backend2": {
"loadBalancer": {
"method": "drr"
},
"servers": {
"server2": {
"weight": 2,
"URL": "http://172.17.0.5:80"
},
"server1": {
"weight": 1,
"url": "http://172.17.0.4:80"
}
}
},
"backend1": {
"loadBalancer": {
"method": "wrr"
},
"circuitBreaker": {
"expression": "NetworkErrorRatio() > 0.5"
},
"servers": {
"server2": {
"weight": 1,
"url": "http://172.17.0.3:80"
},
"server1": {
"weight": 10,
"url": "http://172.17.0.2:80"
}
}
}
}
}
```

View file

@ -1,5 +1,8 @@
# Web Backend
!!! danger "DEPRECATED"
The web provider is deprecated, please use the [api](/configuration/api.md), the [ping](/configuration/ping.md), the [metrics](/configuration/metrics) and the [rest](/configuration/backends/rest.md) provider.
Træfik can be configured:
- using a RESTful api.

View file

@ -0,0 +1,119 @@
# Metrics Definition
## Prometheus
```toml
# Metrics definition
[metrics]
#...
# To enable Traefik to export internal metrics to Prometheus
[metrics.prometheus]
# Buckets for latency metrics
#
# Optional
# Default: [0.1, 0.3, 1.2, 5]
#
buckets = [0.1,0.3,1.2,5.0]
# ...
```
## DataDog
```toml
# Metrics definition
[metrics]
#...
# DataDog metrics exporter type
[metrics.datadog]
# DataDog's address.
#
# Required
# Default: "localhost:8125"
#
address = "localhost:8125"
# DataDog push interval
#
# Optional
# Default: "10s"
#
pushInterval = "10s"
# ...
```
## StatsD
```toml
# Metrics definition
[metrics]
#...
# StatsD metrics exporter type
[metrics.statsd]
# StatD's address.
#
# Required
# Default: "localhost:8125"
#
address = "localhost:8125"
# StatD push interval
#
# Optional
# Default: "10s"
#
pushInterval = "10s"
# ...
```
### InfluxDB
```toml
[web]
# ...
# InfluxDB metrics exporter type
[web.metrics.influxdb]
# InfluxDB's address.
#
# Required
# Default: "localhost:8089"
#
address = "localhost:8089"
# InfluxDB push interval
#
# Optional
# Default: "10s"
#
pushinterval = "10s"
# ...
```
## Statistics
```toml
# Metrics definition
[metrics]
# ...
# Enable more detailed statistics.
[metrics.statistics]
# Number of recent errors logged.
#
# Default: 10
#
recentErrors = 10
# ...
```

View file

@ -0,0 +1,42 @@
# Ping Definition
```toml
# Ping definition
[ping]
# Name of the related entry point
#
# Optional
# Default: "traefik"
#
entryPoint = "traefik"
```
| Path | Method | Description |
|---------|---------------|----------------------------------------------------------------------------------------------------|
| `/ping` | `GET`, `HEAD` | A simple endpoint to check for Træfik process liveness. Return a code `200` with the content: `OK` |
!!! warning
Even if you have authentication configured on entry point, the `/ping` path of the api is excluded from authentication.
### Example
```shell
curl -sv "http://localhost:8080/ping"
```
```shell
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /ping HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 25 Aug 2016 01:35:36 GMT
< Content-Length: 2
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
OK
```

View file

@ -162,3 +162,104 @@ func (s *SimpleSuite) TestRequestAcceptGraceTimeout(c *check.C) {
c.Fatal("Traefik did not terminate in time")
}
}
func (s *SimpleSuite) TestApiOnSameEntryPoint(c *check.C) {
s.createComposeProject(c, "base")
s.composeProject.Start(c)
cmd, output := s.traefikCmd("--defaultEntryPoints=http", "--entryPoints=Name:http Address::8000", "--api.entryPoint=http", "--debug", "--docker")
defer output(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// TODO validate : run on 80
// Expected a 404 as we did not configure anything
err = try.GetRequest("http://127.0.0.1:8000/test", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/api", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/api/providers", 1*time.Second, try.BodyContains("PathPrefix"))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
}
func (s *SimpleSuite) TestNoAuthOnPing(c *check.C) {
s.createComposeProject(c, "base")
s.composeProject.Start(c)
cmd, output := s.traefikCmd(withConfigFile("./fixtures/simple_auth.toml"))
defer output(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
err = try.GetRequest("http://127.0.0.1:8001/api", 1*time.Second, try.StatusCodeIs(http.StatusUnauthorized))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8001/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
}
func (s *SimpleSuite) TestWebCompatibilityWithoutPath(c *check.C) {
s.createComposeProject(c, "base")
s.composeProject.Start(c)
cmd, output := s.traefikCmd("--defaultEntryPoints=http", "--entryPoints=Name:http Address::8000", "--web", "--debug", "--docker")
defer output(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// TODO validate : run on 80
// Expected a 404 as we did not configure anything
err = try.GetRequest("http://127.0.0.1:8000/test", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("PathPrefix"))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
}
func (s *SimpleSuite) TestWebCompatibilityWithPath(c *check.C) {
s.createComposeProject(c, "base")
s.composeProject.Start(c)
cmd, output := s.traefikCmd("--defaultEntryPoints=http", "--entryPoints=Name:http Address::8000", "--web.path=/test", "--debug", "--docker")
defer output(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// TODO validate : run on 80
// Expected a 404 as we did not configure anything
err = try.GetRequest("http://127.0.0.1:8000/notfound", 1*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/test/api", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/test/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/test/api/providers", 1*time.Second, try.BodyContains("PathPrefix"))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
}

View file

@ -0,0 +1,16 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
[entryPoints.traefik]
address = ":8001"
[entryPoints.traefik.auth.basic]
users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
[api]
[ping]

View file

@ -0,0 +1,5 @@
whoami1:
image: emilevauge/whoami
labels:
- traefik.enable=true
- traefik.frontend.rule=PathPrefix:/whoami

View file

@ -1,9 +1,11 @@
package metrics
import (
"github.com/containous/mux"
"github.com/containous/traefik/types"
"github.com/go-kit/kit/metrics/prometheus"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
@ -14,6 +16,14 @@ const (
retriesTotalName = metricNamePrefix + "backend_retries_total"
)
// PrometheusHandler expose Prometheus routes
type PrometheusHandler struct{}
// AddRoutes add Prometheus routes on a router
func (h PrometheusHandler) AddRoutes(router *mux.Router) {
router.Methods("GET").Path("/metrics").Handler(promhttp.Handler())
}
// RegisterPrometheus registers all Prometheus metrics.
// It must be called only once and failing to register the metrics will lead to a panic.
func RegisterPrometheus(config *types.Prometheus) Registry {

View file

@ -84,7 +84,11 @@ pages:
- 'Backend: Marathon': 'configuration/backends/marathon.md'
- 'Backend: Mesos': 'configuration/backends/mesos.md'
- 'Backend: Rancher': 'configuration/backends/rancher.md'
- 'Backend: Rest': 'configuration/backends/rest.md'
- 'Backend: Zookeeper': 'configuration/backends/zookeeper.md'
- 'API / Dashboard': 'configuration/api.md'
- 'Ping': 'configuration/ping.md'
- 'Metrics': 'configuration/metrics.md'
- User Guides:
- 'Configuration Examples': 'user-guide/examples.md'
- 'Swarm Mode Cluster': 'user-guide/swarm-mode.md'

21
ping/ping.go Normal file
View file

@ -0,0 +1,21 @@
package ping
import (
"fmt"
"net/http"
"github.com/containous/mux"
)
//Handler expose ping routes
type Handler struct {
EntryPoint string `description:"Ping entryPoint" export:"true"`
}
// AddRoutes add ping routes on a router
func (g Handler) AddRoutes(router *mux.Router) {
router.Methods("GET", "HEAD").Path("/ping").
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
fmt.Fprint(response, "OK")
})
}

65
provider/rest/rest.go Normal file
View file

@ -0,0 +1,65 @@
package rest
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/containous/mux"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"github.com/unrolled/render"
)
// Provider is a provider.Provider implementation that provides a Rest API
type Provider struct {
configurationChan chan<- types.ConfigMessage
EntryPoint string `description:"EntryPoint" export:"true"`
CurrentConfigurations *safe.Safe
}
var templatesRenderer = render.New(render.Options{Directory: "nowhere"})
// AddRoutes add rest provider routes on a router
func (p *Provider) AddRoutes(systemRouter *mux.Router) {
systemRouter.Methods("PUT").Path("/api/providers/{provider}").HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
// TODO: Deprecated configuration - Need to be removed in the future
if vars["provider"] != "web" && vars["provider"] != "rest" {
response.WriteHeader(http.StatusBadRequest)
fmt.Fprint(response, "Only 'rest' provider can be updated through the REST API")
return
} else if vars["provider"] == "web" {
log.Warn("The provider web is deprecated. Please use /rest instead")
}
configuration := new(types.Configuration)
body, _ := ioutil.ReadAll(request.Body)
err := json.Unmarshal(body, configuration)
if err == nil {
// TODO: Deprecated configuration - Change to `rest` in the future
p.configurationChan <- types.ConfigMessage{ProviderName: "web", Configuration: configuration}
p.getConfigHandler(response, request)
} else {
log.Errorf("Error parsing configuration %+v", err)
http.Error(response, fmt.Sprintf("%+v", err), http.StatusBadRequest)
}
})
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ types.Constraints) error {
p.configurationChan = configurationChan
return nil
}
func (p *Provider) getConfigHandler(response http.ResponseWriter, request *http.Request) {
currentConfigurations := p.CurrentConfigurations.Get().(types.Configurations)
err := templatesRenderer.JSON(response, http.StatusOK, currentConfigurations)
if err != nil {
log.Error(err)
}
}

View file

@ -1,330 +0,0 @@
package web
import (
"encoding/json"
"expvar"
"fmt"
"io/ioutil"
"net/http"
"runtime"
"github.com/containous/mux"
"github.com/containous/traefik/autogen"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares"
mauth "github.com/containous/traefik/middlewares/auth"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"github.com/containous/traefik/version"
"github.com/elazarl/go-bindata-assetfs"
"github.com/prometheus/client_golang/prometheus/promhttp"
thoas_stats "github.com/thoas/stats"
"github.com/unrolled/render"
"github.com/urfave/negroni"
)
// Provider is a provider.Provider implementation that provides the UI
type Provider struct {
Address string `description:"Web administration port" export:"true"`
CertFile string `description:"SSL certificate" export:"true"`
KeyFile string `description:"SSL certificate" export:"true"`
ReadOnly bool `description:"Enable read only API" export:"true"`
Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"`
Path string `description:"Root path for dashboard and API"`
Auth *types.Auth `export:"true"`
Debug bool `export:"true"`
CurrentConfigurations *safe.Safe
Stats *thoas_stats.Stats
StatsRecorder *middlewares.StatsRecorder
}
var (
templatesRenderer = render.New(render.Options{
Directory: "nowhere",
})
)
func init() {
expvar.Publish("Goroutines", expvar.Func(goroutines))
}
func goroutines() interface{} {
return runtime.NumGoroutine()
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ types.Constraints) error {
systemRouter := mux.NewRouter()
if provider.Path != "/" {
systemRouter.Methods("GET").Path("/").HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
http.Redirect(response, request, provider.Path, 302)
})
}
// Prometheus route
if provider.Metrics != nil && provider.Metrics.Prometheus != nil {
systemRouter.Methods("GET").Path(provider.Path + "metrics").Handler(promhttp.Handler())
}
// health route
systemRouter.Methods("GET").Path(provider.Path + "health").HandlerFunc(provider.getHealthHandler)
// ping route
systemRouter.Methods("GET", "HEAD").Path(provider.Path + "ping").HandlerFunc(provider.getPingHandler)
// API routes
systemRouter.Methods("GET").Path(provider.Path + "api").HandlerFunc(provider.getConfigHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/version").HandlerFunc(provider.getVersionHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers").HandlerFunc(provider.getConfigHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}").HandlerFunc(provider.getProviderHandler)
systemRouter.Methods("PUT").Path(provider.Path + "api/providers/{provider}").HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if provider.ReadOnly {
response.WriteHeader(http.StatusForbidden)
fmt.Fprint(response, "REST API is in read-only mode")
return
}
vars := mux.Vars(request)
if vars["provider"] != "web" {
response.WriteHeader(http.StatusBadRequest)
fmt.Fprint(response, "Only 'web' provider can be updated through the REST API")
return
}
configuration := new(types.Configuration)
body, _ := ioutil.ReadAll(request.Body)
err := json.Unmarshal(body, configuration)
if err == nil {
configurationChan <- types.ConfigMessage{ProviderName: "web", Configuration: configuration}
provider.getConfigHandler(response, request)
} else {
log.Errorf("Error parsing configuration %+v", err)
http.Error(response, fmt.Sprintf("%+v", err), http.StatusBadRequest)
}
})
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/backends").HandlerFunc(provider.getBackendsHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/backends/{backend}").HandlerFunc(provider.getBackendHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/backends/{backend}/servers").HandlerFunc(provider.getServersHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/backends/{backend}/servers/{server}").HandlerFunc(provider.getServerHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/frontends").HandlerFunc(provider.getFrontendsHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/frontends/{frontend}").HandlerFunc(provider.getFrontendHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/frontends/{frontend}/routes").HandlerFunc(provider.getRoutesHandler)
systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/frontends/{frontend}/routes/{route}").HandlerFunc(provider.getRouteHandler)
// Expose dashboard
systemRouter.Methods("GET").Path(provider.Path).HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
http.Redirect(response, request, provider.Path+"dashboard/", 302)
})
systemRouter.Methods("GET").PathPrefix(provider.Path + "dashboard/").
Handler(http.StripPrefix(provider.Path+"dashboard/", http.FileServer(&assetfs.AssetFS{Asset: autogen.Asset, AssetInfo: autogen.AssetInfo, AssetDir: autogen.AssetDir, Prefix: "static"})))
// expvars
if provider.Debug {
systemRouter.Methods("GET").Path(provider.Path + "debug/vars").HandlerFunc(expVarHandler)
}
safe.Go(func() {
var err error
var negroniInstance = negroni.New()
if provider.Auth != nil {
authMiddleware, err := mauth.NewAuthenticator(provider.Auth)
if err != nil {
log.Fatal("Error creating Auth: ", err)
}
authMiddlewareWrapper := negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if r.URL.Path == "/ping" {
next.ServeHTTP(w, r)
} else {
authMiddleware.ServeHTTP(w, r, next)
}
})
negroniInstance.Use(authMiddlewareWrapper)
}
negroniInstance.UseHandler(systemRouter)
if len(provider.CertFile) > 0 && len(provider.KeyFile) > 0 {
err = http.ListenAndServeTLS(provider.Address, provider.CertFile, provider.KeyFile, negroniInstance)
} else {
err = http.ListenAndServe(provider.Address, negroniInstance)
}
if err != http.ErrServerClosed {
log.Fatal("Error creating server: ", err)
}
})
return nil
}
// healthResponse combines data returned by thoas/stats with statistics (if
// they are enabled).
type healthResponse struct {
*thoas_stats.Data
*middlewares.Stats
}
func (provider *Provider) getHealthHandler(response http.ResponseWriter, request *http.Request) {
health := &healthResponse{Data: provider.Stats.Data()}
if provider.StatsRecorder != nil {
health.Stats = provider.StatsRecorder.Data()
}
templatesRenderer.JSON(response, http.StatusOK, health)
}
func (provider *Provider) getPingHandler(response http.ResponseWriter, request *http.Request) {
fmt.Fprint(response, "OK")
}
func (provider *Provider) getConfigHandler(response http.ResponseWriter, request *http.Request) {
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
templatesRenderer.JSON(response, http.StatusOK, currentConfigurations)
}
func (provider *Provider) getVersionHandler(response http.ResponseWriter, request *http.Request) {
v := struct {
Version string
Codename string
}{
Version: version.Version,
Codename: version.Codename,
}
templatesRenderer.JSON(response, http.StatusOK, v)
}
func (provider *Provider) getProviderHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
templatesRenderer.JSON(response, http.StatusOK, provider)
} else {
http.NotFound(response, request)
}
}
func (provider *Provider) getBackendsHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
templatesRenderer.JSON(response, http.StatusOK, provider.Backends)
} else {
http.NotFound(response, request)
}
}
func (provider *Provider) getBackendHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
backendID := vars["backend"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, backend)
return
}
}
http.NotFound(response, request)
}
func (provider *Provider) getServersHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
backendID := vars["backend"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, backend.Servers)
return
}
}
http.NotFound(response, request)
}
func (provider *Provider) getServerHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
backendID := vars["backend"]
serverID := vars["server"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
if server, ok := backend.Servers[serverID]; ok {
templatesRenderer.JSON(response, http.StatusOK, server)
return
}
}
}
http.NotFound(response, request)
}
func (provider *Provider) getFrontendsHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
templatesRenderer.JSON(response, http.StatusOK, provider.Frontends)
} else {
http.NotFound(response, request)
}
}
func (provider *Provider) getFrontendHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
frontendID := vars["frontend"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, frontend)
return
}
}
http.NotFound(response, request)
}
func (provider *Provider) getRoutesHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
frontendID := vars["frontend"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, frontend.Routes)
return
}
}
http.NotFound(response, request)
}
func (provider *Provider) getRouteHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
frontendID := vars["frontend"]
routeID := vars["route"]
currentConfigurations := provider.CurrentConfigurations.Get().(types.Configurations)
if provider, ok := currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
if route, ok := frontend.Routes[routeID]; ok {
templatesRenderer.JSON(response, http.StatusOK, route)
return
}
}
}
http.NotFound(response, request)
}
func expVarHandler(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprint(w, "{\n")
first := true
expvar.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprint(w, ",\n")
}
first = false
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
})
fmt.Fprint(w, "\n}\n")
}

View file

@ -103,14 +103,18 @@ func NewServer(globalConfiguration configuration.GlobalConfiguration) *Server {
currentConfigurations := make(types.Configurations)
server.currentConfigurations.Set(currentConfigurations)
server.globalConfiguration = globalConfiguration
if server.globalConfiguration.API != nil {
server.globalConfiguration.API.CurrentConfigurations = &server.currentConfigurations
}
server.routinesPool = safe.NewPool(context.Background())
server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration)
server.lastReceivedConfiguration = safe.New(time.Unix(0, 0))
server.lastConfigs = cmap.New()
server.metricsRegistry = metrics.NewVoidRegistry()
if globalConfiguration.Web != nil && globalConfiguration.Web.Metrics != nil {
server.registerMetricClients(globalConfiguration.Web.Metrics)
if globalConfiguration.Metrics != nil {
server.registerMetricClients(globalConfiguration.Metrics)
}
if globalConfiguration.Cluster != nil {
@ -280,19 +284,21 @@ func (server *Server) startHTTPServers() {
func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newServerEntryPoint *serverEntryPoint) *serverEntryPoint {
serverMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler()}
serverInternalMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler()}
if server.accessLoggerMiddleware != nil {
serverMiddlewares = append(serverMiddlewares, server.accessLoggerMiddleware)
}
if server.metricsRegistry.IsEnabled() {
serverMiddlewares = append(serverMiddlewares, middlewares.NewMetricsWrapper(server.metricsRegistry, newServerEntryPointName))
}
if server.globalConfiguration.Web != nil {
server.globalConfiguration.Web.Stats = thoas_stats.New()
serverMiddlewares = append(serverMiddlewares, server.globalConfiguration.Web.Stats)
if server.globalConfiguration.Web.Statistics != nil {
server.globalConfiguration.Web.StatsRecorder = middlewares.NewStatsRecorder(server.globalConfiguration.Web.Statistics.RecentErrors)
serverMiddlewares = append(serverMiddlewares, server.globalConfiguration.Web.StatsRecorder)
if server.globalConfiguration.API != nil {
server.globalConfiguration.API.Stats = thoas_stats.New()
serverMiddlewares = append(serverMiddlewares, server.globalConfiguration.API.Stats)
if server.globalConfiguration.API.Statistics != nil {
server.globalConfiguration.API.StatsRecorder = middlewares.NewStatsRecorder(server.globalConfiguration.API.Statistics.RecentErrors)
serverMiddlewares = append(serverMiddlewares, server.globalConfiguration.API.StatsRecorder)
}
}
if server.globalConfiguration.EntryPoints[newServerEntryPointName].Auth != nil {
authMiddleware, err := mauth.NewAuthenticator(server.globalConfiguration.EntryPoints[newServerEntryPointName].Auth)
@ -300,6 +306,7 @@ func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newS
log.Fatal("Error starting server: ", err)
}
serverMiddlewares = append(serverMiddlewares, authMiddleware)
serverInternalMiddlewares = append(serverInternalMiddlewares, authMiddleware)
}
if server.globalConfiguration.EntryPoints[newServerEntryPointName].Compress {
serverMiddlewares = append(serverMiddlewares, &middlewares.Compress{})
@ -310,8 +317,9 @@ func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newS
log.Fatal("Error starting server: ", err)
}
serverMiddlewares = append(serverMiddlewares, ipWhitelistMiddleware)
serverInternalMiddlewares = append(serverInternalMiddlewares, ipWhitelistMiddleware)
}
newSrv, listener, err := server.prepareServer(newServerEntryPointName, server.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares...)
newSrv, listener, err := server.prepareServer(newServerEntryPointName, server.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares, serverInternalMiddlewares)
if err != nil {
log.Fatal("Error preparing server: ", err)
}
@ -500,10 +508,9 @@ func (server *Server) configureProviders() {
if server.globalConfiguration.File != nil {
server.providers = append(server.providers, server.globalConfiguration.File)
}
if server.globalConfiguration.Web != nil {
server.globalConfiguration.Web.CurrentConfigurations = &server.currentConfigurations
server.globalConfiguration.Web.Debug = server.globalConfiguration.Debug
server.providers = append(server.providers, server.globalConfiguration.Web)
if server.globalConfiguration.Rest != nil {
server.providers = append(server.providers, server.globalConfiguration.Rest)
server.globalConfiguration.Rest.CurrentConfigurations = &server.currentConfigurations
}
if server.globalConfiguration.Consul != nil {
server.providers = append(server.providers, server.globalConfiguration.Consul)
@ -689,7 +696,27 @@ func (server *Server) startServer(serverEntryPoint *serverEntryPoint, globalConf
}
}
func (server *Server) prepareServer(entryPointName string, entryPoint *configuration.EntryPoint, router *middlewares.HandlerSwitcher, middlewares ...negroni.Handler) (*http.Server, net.Listener, error) {
func (server *Server) addInternalRoutes(entryPointName string, router *mux.Router) {
if server.globalConfiguration.Metrics != nil && server.globalConfiguration.Metrics.Prometheus != nil && server.globalConfiguration.Metrics.Prometheus.EntryPoint == entryPointName {
metrics.PrometheusHandler{}.AddRoutes(router)
}
if server.globalConfiguration.Rest != nil && server.globalConfiguration.Rest.EntryPoint == entryPointName {
server.globalConfiguration.Rest.AddRoutes(router)
}
if server.globalConfiguration.API != nil && server.globalConfiguration.API.EntryPoint == entryPointName {
server.globalConfiguration.API.AddRoutes(router)
}
}
func (server *Server) addInternalPublicRoutes(entryPointName string, router *mux.Router) {
if server.globalConfiguration.Ping != nil && server.globalConfiguration.Ping.EntryPoint != "" && server.globalConfiguration.Ping.EntryPoint == entryPointName {
server.globalConfiguration.Ping.AddRoutes(router)
}
}
func (server *Server) prepareServer(entryPointName string, entryPoint *configuration.EntryPoint, router *middlewares.HandlerSwitcher, middlewares []negroni.Handler, internalMiddlewares []negroni.Handler) (*http.Server, net.Listener, error) {
readTimeout, writeTimeout, idleTimeout := buildServerTimeouts(server.globalConfiguration)
log.Infof("Preparing server %s %+v with readTimeout=%s writeTimeout=%s idleTimeout=%s", entryPointName, entryPoint, readTimeout, writeTimeout, idleTimeout)
@ -700,6 +727,14 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
}
n.UseHandler(router)
path := "/"
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Path != "" {
path = server.globalConfiguration.Web.Path
}
internalMuxRouter := server.buildInternalRouter(entryPointName, path, internalMiddlewares)
internalMuxRouter.NotFoundHandler = n
tlsConfig, err := server.createTLSConfig(entryPointName, entryPoint.TLS, router)
if err != nil {
log.Errorf("Error creating TLS config: %s", err)
@ -715,7 +750,7 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
if entryPoint.ProxyProtocol != nil {
IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs, entryPoint.ProxyProtocol.Insecure)
if err != nil {
return nil, nil, fmt.Errorf("Error creating whitelist: %s", err)
return nil, nil, fmt.Errorf("error creating whitelist: %s", err)
}
log.Infof("Enabling ProxyProtocol for trusted IPs %v", entryPoint.ProxyProtocol.TrustedIPs)
listener = &proxyproto.Listener{
@ -723,7 +758,7 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
SourceCheck: func(addr net.Addr) (bool, error) {
ip, ok := addr.(*net.TCPAddr)
if !ok {
return false, fmt.Errorf("Type error %v", addr)
return false, fmt.Errorf("type error %v", addr)
}
return IPs.ContainsIP(ip.IP)
},
@ -732,7 +767,7 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
return &http.Server{
Addr: entryPoint.Address,
Handler: n,
Handler: internalMuxRouter,
TLSConfig: tlsConfig,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
@ -742,6 +777,31 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
nil
}
func (server *Server) buildInternalRouter(entryPointName, path string, internalMiddlewares []negroni.Handler) *mux.Router {
internalMuxRouter := mux.NewRouter()
internalMuxRouter.StrictSlash(true)
internalMuxRouter.SkipClean(true)
internalMuxSubrouter := internalMuxRouter.PathPrefix(path).Subrouter()
internalMuxSubrouter.StrictSlash(true)
internalMuxSubrouter.SkipClean(true)
server.addInternalRoutes(entryPointName, internalMuxSubrouter)
internalMuxRouter.Walk(wrapRoute(internalMiddlewares))
server.addInternalPublicRoutes(entryPointName, internalMuxSubrouter)
return internalMuxRouter
}
// wrapRoute with middlewares
func wrapRoute(middlewares []negroni.Handler) func(*mux.Route, *mux.Router, []*mux.Route) error {
return func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
middles := append(middlewares, negroni.Wrap(route.GetHandler()))
route.Handler(negroni.New(middles...))
return nil
}
}
func buildServerTimeouts(globalConfig configuration.GlobalConfiguration) (readTimeout, writeTimeout, idleTimeout time.Duration) {
readTimeout = time.Duration(0)
writeTimeout = time.Duration(0)

View file

@ -142,7 +142,7 @@ func TestPrepareServerTimeouts(t *testing.T) {
router := middlewares.NewHandlerSwitcher(mux.NewRouter())
srv := NewServer(test.globalConfig)
httpServer, _, err := srv.prepareServer(entryPointName, entryPoint, router)
httpServer, _, err := srv.prepareServer(entryPointName, entryPoint, router, nil, nil)
if err != nil {
t.Fatalf("Unexpected error when preparing srv: %s", err)
}
@ -597,7 +597,7 @@ func TestServerEntryPointWhitelistConfig(t *testing.T) {
srv.serverEntryPoints = srv.buildEntryPoints(srv.globalConfiguration)
srvEntryPoint := srv.setupServerEntryPoint("test", srv.serverEntryPoints["test"])
handler := srvEntryPoint.httpServer.Handler.(*negroni.Negroni)
handler := srvEntryPoint.httpServer.Handler.(*mux.Router).NotFoundHandler.(*negroni.Negroni)
found := false
for _, handler := range handler.Handlers() {
if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhiteLister)(nil)) {

View file

@ -376,6 +376,7 @@ type Metrics struct {
// Prometheus can contain specific configuration used by the Prometheus Metrics exporter
type Prometheus struct {
Buckets Buckets `description:"Buckets for latency metrics" export:"true"`
EntryPoint string `description:"EntryPoint" export:"true"`
}
// Datadog contains address and metrics pushing interval configuration

View file

@ -2,11 +2,14 @@ package version
import (
"context"
"net/http"
"net/url"
"github.com/containous/mux"
"github.com/containous/traefik/log"
"github.com/google/go-github/github"
goversion "github.com/hashicorp/go-version"
"github.com/unrolled/render"
)
var (
@ -18,6 +21,30 @@ var (
BuildDate = "I don't remember exactly"
)
// Handler expose version routes
type Handler struct{}
var (
templatesRenderer = render.New(render.Options{
Directory: "nowhere",
})
)
// AddRoutes add version routes on a router
func (v Handler) AddRoutes(router *mux.Router) {
router.Methods("GET").Path("/api/version").
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
v := struct {
Version string
Codename string
}{
Version: Version,
Codename: Codename,
}
templatesRenderer.JSON(response, http.StatusOK, v)
})
}
// CheckNewVersion checks if a new version is available
func CheckNewVersion() {
if Version == "dev" {