From 27d1b468354575d74a2c93d9d94a852a7abdd636 Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Thu, 9 Nov 2017 16:12:04 +0100 Subject: [PATCH] Split Web into API/Dashboard, ping, metric and Rest Provider --- api/dashboard.go | 23 ++ api/debug.go | 39 +++ api/handler.go | 250 +++++++++++++ .../anonymize/anonymize_config_test.go | 15 +- cmd/traefik/configuration.go | 52 ++- cmd/traefik/traefik.go | 20 +- configuration/configuration.go | 92 ++++- docs/configuration/api.md | 203 +++++++++++ docs/configuration/backends/rest.md | 91 +++++ docs/configuration/backends/web.md | 3 + docs/configuration/metrics.md | 119 +++++++ docs/configuration/ping.md | 42 +++ integration/basic_test.go | 101 ++++++ integration/fixtures/simple_auth.toml | 16 + integration/resources/compose/base.yml | 5 + metrics/prometheus.go | 10 + mkdocs.yml | 4 + ping/ping.go | 21 ++ provider/rest/rest.go | 65 ++++ provider/web/web.go | 330 ------------------ server/server.go | 94 ++++- server/server_test.go | 4 +- types/types.go | 3 +- version/version.go | 27 ++ 24 files changed, 1252 insertions(+), 377 deletions(-) create mode 100644 api/dashboard.go create mode 100644 api/debug.go create mode 100644 api/handler.go create mode 100644 docs/configuration/api.md create mode 100644 docs/configuration/backends/rest.md create mode 100644 docs/configuration/metrics.md create mode 100644 docs/configuration/ping.md create mode 100644 integration/fixtures/simple_auth.toml create mode 100644 integration/resources/compose/base.yml create mode 100644 ping/ping.go create mode 100644 provider/rest/rest.go delete mode 100644 provider/web/web.go diff --git a/api/dashboard.go b/api/dashboard.go new file mode 100644 index 000000000..268b179af --- /dev/null +++ b/api/dashboard.go @@ -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"}))) + +} diff --git a/api/debug.go b/api/debug.go new file mode 100644 index 000000000..61db00a8f --- /dev/null +++ b/api/debug.go @@ -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") + }) +} diff --git a/api/handler.go b/api/handler.go new file mode 100644 index 000000000..0420039db --- /dev/null +++ b/api/handler.go @@ -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) + } +} diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go index 1fa91c9fd..61c2fd576 100644 --- a/cmd/traefik/anonymize/anonymize_config_test.go +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -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{ diff --git a/cmd/traefik/configuration.go b/cmd/traefik/configuration.go index 0ec5ae26f..c2b4d0cfb 100644 --- a/cmd/traefik/configuration.go +++ b/cmd/traefik/configuration.go @@ -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{ diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index e420dca3e..35fbd821d 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -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 { diff --git a/configuration/configuration.go b/configuration/configuration.go index 647dc4bfa..929f3fb32 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -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 += "/" } diff --git a/docs/configuration/api.md b/docs/configuration/api.md new file mode 100644 index 000000000..60b608b7e --- /dev/null +++ b/docs/configuration/api.md @@ -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 | diff --git a/docs/configuration/backends/rest.md b/docs/configuration/backends/rest.md new file mode 100644 index 000000000..8c1dbe0e3 --- /dev/null +++ b/docs/configuration/backends/rest.md @@ -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" + } + } + } + } +} +``` \ No newline at end of file diff --git a/docs/configuration/backends/web.md b/docs/configuration/backends/web.md index 5e0573d32..93cce9676 100644 --- a/docs/configuration/backends/web.md +++ b/docs/configuration/backends/web.md @@ -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. diff --git a/docs/configuration/metrics.md b/docs/configuration/metrics.md new file mode 100644 index 000000000..1f842f75c --- /dev/null +++ b/docs/configuration/metrics.md @@ -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 + + # ... +``` diff --git a/docs/configuration/ping.md b/docs/configuration/ping.md new file mode 100644 index 000000000..bfe27b79a --- /dev/null +++ b/docs/configuration/ping.md @@ -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 +``` \ No newline at end of file diff --git a/integration/basic_test.go b/integration/basic_test.go index 128c99d75..2d78343af 100644 --- a/integration/basic_test.go +++ b/integration/basic_test.go @@ -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) +} diff --git a/integration/fixtures/simple_auth.toml b/integration/fixtures/simple_auth.toml new file mode 100644 index 000000000..4131e42af --- /dev/null +++ b/integration/fixtures/simple_auth.toml @@ -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] diff --git a/integration/resources/compose/base.yml b/integration/resources/compose/base.yml new file mode 100644 index 000000000..8af2e9a99 --- /dev/null +++ b/integration/resources/compose/base.yml @@ -0,0 +1,5 @@ +whoami1: + image: emilevauge/whoami + labels: + - traefik.enable=true + - traefik.frontend.rule=PathPrefix:/whoami diff --git a/metrics/prometheus.go b/metrics/prometheus.go index 52d0b4c55..266418f7d 100644 --- a/metrics/prometheus.go +++ b/metrics/prometheus.go @@ -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 { diff --git a/mkdocs.yml b/mkdocs.yml index a7fe166b5..1e7f3fce1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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' diff --git a/ping/ping.go b/ping/ping.go new file mode 100644 index 000000000..212415797 --- /dev/null +++ b/ping/ping.go @@ -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") + }) +} diff --git a/provider/rest/rest.go b/provider/rest/rest.go new file mode 100644 index 000000000..65dbe1df9 --- /dev/null +++ b/provider/rest/rest.go @@ -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) + } +} diff --git a/provider/web/web.go b/provider/web/web.go deleted file mode 100644 index 103a3ff02..000000000 --- a/provider/web/web.go +++ /dev/null @@ -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") -} diff --git a/server/server.go b/server/server.go index 24c8507d7..de620b58b 100644 --- a/server/server.go +++ b/server/server.go @@ -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) diff --git a/server/server_test.go b/server/server_test.go index f8aa82c2b..372c0fdb4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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)) { diff --git a/types/types.go b/types/types.go index e27df5754..1335132dd 100644 --- a/types/types.go +++ b/types/types.go @@ -375,7 +375,8 @@ 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"` + Buckets Buckets `description:"Buckets for latency metrics" export:"true"` + EntryPoint string `description:"EntryPoint" export:"true"` } // Datadog contains address and metrics pushing interval configuration diff --git a/version/version.go b/version/version.go index 500b324cd..605c10216 100644 --- a/version/version.go +++ b/version/version.go @@ -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" {