From c22598c8ffb91ded01c3d99fea37b613ba2ba536 Mon Sep 17 00:00:00 2001 From: emile Date: Fri, 29 Jan 2016 20:34:17 +0100 Subject: [PATCH 1/4] Add multiple entry points support, add entry point redirection --- Makefile | 9 + adapters.go | 8 - cmd.go | 12 +- configuration.go | 139 +++++++- glide.yaml | 3 +- middlewares/rewrite.go | 31 ++ server.go | 307 +++++++++++------- types/types.go | 1 + .../frontend-monitor/frontend-monitor.html | 1 + 9 files changed, 366 insertions(+), 145 deletions(-) create mode 100644 middlewares/rewrite.go diff --git a/Makefile b/Makefile index 0ac639baa..4d376b004 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ TRAEFIK_ENVS := \ -e TESTFLAGS \ -e CIRCLECI + +SRCS = $(shell git ls-files '*.go' | grep -v '^external/') + BIND_DIR := "dist" TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/emilevauge/traefik/$(BIND_DIR)" @@ -78,3 +81,9 @@ generate-webui: mkdir -p static docker run --rm -v "$$PWD/static":'/src/static' traefik-webui gulp echo 'For more informations show `webui/readme.md`' > $$PWD/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md + +lint: + $(foreach file,$(SRCS),golint $(file) || exit;) + +fmt: + gofmt -s -l -w $(SRCS) \ No newline at end of file diff --git a/adapters.go b/adapters.go index dcd7e5f84..638863132 100644 --- a/adapters.go +++ b/adapters.go @@ -7,7 +7,6 @@ import ( "net/http" log "github.com/Sirupsen/logrus" - "github.com/gorilla/mux" ) // OxyLogger implements oxy Logger interface with logrus. @@ -33,10 +32,3 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) //templatesRenderer.HTML(w, http.StatusNotFound, "notFound", nil) } - -// LoadDefaultConfig returns a default gorrilla.mux router from the specified configuration. -func LoadDefaultConfig(globalConfiguration GlobalConfiguration) *mux.Router { - router := mux.NewRouter() - router.NotFoundHandler = http.HandlerFunc(notFoundHandler) - return router -} diff --git a/cmd.go b/cmd.go index 9d7c4f678..b39a1e6f1 100644 --- a/cmd.go +++ b/cmd.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "encoding/json" log "github.com/Sirupsen/logrus" "github.com/emilevauge/traefik/middlewares" "github.com/emilevauge/traefik/provider" @@ -49,6 +50,7 @@ var arguments = struct { boltdb bool }{ GlobalConfiguration{ + EntryPoints: make(EntryPoints), Docker: &provider.Docker{ TLS: &provider.DockerTLS{}, }, @@ -78,7 +80,8 @@ func init() { traefikCmd.PersistentFlags().StringP("graceTimeOut", "g", "10", "Timeout in seconds. Duration to give active requests a chance to finish during hot-reloads") traefikCmd.PersistentFlags().String("accessLogsFile", "log/access.log", "Access logs file") traefikCmd.PersistentFlags().String("traefikLogsFile", "log/traefik.log", "Traefik logs file") - traefikCmd.PersistentFlags().Var(&arguments.Certificates, "certificates", "SSL certificates and keys pair, ie 'tests/traefik.crt,tests/traefik.key'. You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI") + traefikCmd.PersistentFlags().Var(&arguments.EntryPoints, "entryPoints", "Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'") + traefikCmd.PersistentFlags().Var(&arguments.DefaultEntryPoints, "defaultEntryPoints", "Entrypoints to be used by frontends that do not specify any entrypoint") traefikCmd.PersistentFlags().StringP("logLevel", "l", "ERROR", "Log level") traefikCmd.PersistentFlags().DurationVar(&arguments.ProvidersThrottleDuration, "providersThrottleDuration", time.Duration(2*time.Second), "Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time.") traefikCmd.PersistentFlags().Int("maxIdleConnsPerHost", 0, "If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used") @@ -138,13 +141,13 @@ func init() { viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile")) viper.BindPFlag("port", traefikCmd.PersistentFlags().Lookup("port")) viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut")) - // viper.BindPFlag("certificates", TraefikCmd.PersistentFlags().Lookup("certificates")) + //viper.BindPFlag("defaultEntryPoints", traefikCmd.PersistentFlags().Lookup("defaultEntryPoints")) viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel")) // TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105 viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration")) viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost")) - viper.SetDefault("certificates", &Certificates{}) viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second)) + viper.SetDefault("logLevel", "ERROR") } func run() { @@ -176,7 +179,8 @@ func run() { } else { log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) } - log.Debugf("Global configuration loaded %+v", globalConfiguration) + jsonConf, _ := json.Marshal(globalConfiguration) + log.Debugf("Global configuration loaded %s", string(jsonConf)) server := NewServer(*globalConfiguration) server.Start() defer server.Close() diff --git a/configuration.go b/configuration.go index 5db36deaf..e02a1fbb5 100644 --- a/configuration.go +++ b/configuration.go @@ -11,17 +11,18 @@ import ( "github.com/emilevauge/traefik/types" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + "regexp" ) // GlobalConfiguration holds global configuration (with providers, etc.). // It's populated from the traefik configuration file passed as an argument to the binary. type GlobalConfiguration struct { - Port string GraceTimeOut int64 AccessLogsFile string TraefikLogsFile string - Certificates Certificates LogLevel string + EntryPoints EntryPoints + DefaultEntryPoints DefaultEntryPoints ProvidersThrottleDuration time.Duration MaxIdleConnsPerHost int Docker *provider.Docker @@ -34,6 +35,110 @@ type GlobalConfiguration struct { Boltdb *provider.BoltDb } +// DefaultEntryPoints holds default entry points +type DefaultEntryPoints []string + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (dep *DefaultEntryPoints) String() string { + return fmt.Sprintf("%#v", dep) +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (dep *DefaultEntryPoints) Set(value string) error { + entrypoints := strings.Split(value, ",") + if len(entrypoints) == 0 { + return errors.New("Bad DefaultEntryPoints format: " + value) + } + for _, entrypoint := range entrypoints { + *dep = append(*dep, entrypoint) + } + return nil +} + +// Type is type of the struct +func (dep *DefaultEntryPoints) Type() string { + return fmt.Sprint("defaultentrypoints²") +} + +// EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...) +type EntryPoints map[string]*EntryPoint + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (ep *EntryPoints) String() string { + return "" +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (ep *EntryPoints) Set(value string) error { + regex := regexp.MustCompile("(?:Name:(?P\\S*))\\s*(?:Address:(?P
\\S*))?\\s*(?:TLS:(?P\\S*))?\\s*(?:Redirect.EntryPoint:(?P\\S*))?\\s*(?:Redirect.Regex:(?P\\S*))?\\s*(?:Redirect.Replacement:(?P\\S*))?") + match := regex.FindAllStringSubmatch(value, -1) + if match == nil { + return errors.New("Bad EntryPoints format: " + value) + } + matchResult := match[0] + result := make(map[string]string) + for i, name := range regex.SubexpNames() { + if i != 0 { + result[name] = matchResult[i] + } + } + var tls *TLS + if len(result["TLS"]) > 0 { + certs := Certificates{} + certs.Set(result["TLS"]) + tls = &TLS{ + Certificates: certs, + } + } + var redirect *Redirect + if len(result["RedirectEntryPoint"]) > 0 || len(result["RedirectRegex"]) > 0 || len(result["RedirectReplacement"]) > 0 { + redirect = &Redirect{ + EntryPoint: result["RedirectEntryPoint"], + Regex: result["RedirectRegex"], + Replacement: result["RedirectReplacement"], + } + } + + (*ep)[result["Name"]] = &EntryPoint{ + Address: result["Address"], + TLS: tls, + Redirect: redirect, + } + + return nil +} + +// Type is type of the struct +func (ep *EntryPoints) Type() string { + return fmt.Sprint("entrypoints²") +} + +// EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...) +type EntryPoint struct { + Network string + Address string + TLS *TLS + Redirect *Redirect +} + +// Redirect configures a redirection of an entry point to another, or to an URL +type Redirect struct { + EntryPoint string + Regex string + Replacement string +} + +// TLS configures TLS for an entry point +type TLS struct { + Certificates Certificates +} + // Certificates defines traefik certificates type type Certificates []Certificate @@ -74,14 +179,7 @@ type Certificate struct { // NewGlobalConfiguration returns a GlobalConfiguration with default values. func NewGlobalConfiguration() *GlobalConfiguration { - globalConfiguration := new(GlobalConfiguration) - // default values - globalConfiguration.Port = ":80" - globalConfiguration.GraceTimeOut = 10 - globalConfiguration.LogLevel = "ERROR" - globalConfiguration.ProvidersThrottleDuration = time.Duration(2 * time.Second) - - return globalConfiguration + return new(GlobalConfiguration) } // LoadConfiguration returns a GlobalConfiguration. @@ -101,8 +199,12 @@ func LoadConfiguration() *GlobalConfiguration { if err := viper.ReadInConfig(); err != nil { fmtlog.Fatalf("Error reading file: %s", err) } - if len(arguments.Certificates) > 0 { - viper.Set("certificates", arguments.Certificates) + + if len(arguments.EntryPoints) > 0 { + viper.Set("entryPoints", arguments.EntryPoints) + } + if len(arguments.DefaultEntryPoints) > 0 { + viper.Set("defaultEntryPoints", arguments.DefaultEntryPoints) } if arguments.web { viper.Set("web", arguments.Web) @@ -135,6 +237,19 @@ func LoadConfiguration() *GlobalConfiguration { fmtlog.Fatalf("Error reading file: %s", err) } + if len(configuration.EntryPoints) == 0 { + configuration.EntryPoints = make(map[string]*EntryPoint) + configuration.EntryPoints["http"] = &EntryPoint{ + Address: ":80", + } + configuration.DefaultEntryPoints = []string{"http"} + } + + if configuration.File != nil && len(configuration.File.Filename) == 0 { + // no filename, setting to global config file + configuration.File.Filename = viper.ConfigFileUsed() + } + return configuration } diff --git a/glide.yaml b/glide.yaml index 376b55c35..9a91f4b96 100644 --- a/glide.yaml +++ b/glide.yaml @@ -145,7 +145,7 @@ import: - package: github.com/donovanhide/eventsource ref: d8a3071799b98cacd30b6da92f536050ccfe6da4 - package: github.com/golang/glog - ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f + ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f - package: github.com/spf13/cast ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778 - package: github.com/mitchellh/mapstructure @@ -161,4 +161,5 @@ import: - package: github.com/spf13/cobra subpackages: - /cobra + - package: github.com/vulcand/vulcand/plugin/rewrite diff --git a/middlewares/rewrite.go b/middlewares/rewrite.go new file mode 100644 index 000000000..eaaff6bec --- /dev/null +++ b/middlewares/rewrite.go @@ -0,0 +1,31 @@ +package middlewares + +import ( + log "github.com/Sirupsen/logrus" + "github.com/vulcand/vulcand/plugin/rewrite" + "net/http" +) + +// Rewrite is a middleware that allows redirections +type Rewrite struct { + rewriter *rewrite.Rewrite +} + +// NewRewrite creates a Rewrite middleware +func NewRewrite(regex, replacement string, redirect bool) (*Rewrite, error) { + rewriter, err := rewrite.NewRewrite(regex, replacement, false, redirect) + if err != nil { + return nil, err + } + return &Rewrite{rewriter: rewriter}, nil +} + +// +func (rewrite *Rewrite) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + handler, err := rewrite.rewriter.NewHandler(next) + if err != nil { + log.Error("Error in rewrite middleware ", err) + return + } + handler.ServeHTTP(rw, r) +} diff --git a/server.go b/server.go index 5b70fa1fa..530df5107 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ package main import ( "crypto/tls" + "encoding/json" "errors" log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" @@ -16,12 +17,12 @@ import ( "github.com/mailgun/oxy/cbreaker" "github.com/mailgun/oxy/forward" "github.com/mailgun/oxy/roundrobin" - "github.com/spf13/viper" "net/http" "net/url" "os" "os/signal" "reflect" + "regexp" "sync" "syscall" "time" @@ -31,11 +32,10 @@ var oxyLogger = &OxyLogger{} // Server is the reverse-proxy/load-balancer engine type Server struct { - srv *manners.GracefulServer - configurationRouter *mux.Router + serverEntryPoints map[string]serverEntryPoint configurationChan chan types.ConfigMessage - configurationChanValidated chan types.ConfigMessage - sigs chan os.Signal + configurationValidatedChan chan types.ConfigMessage + signals chan os.Signal stopChan chan bool providers []provider.Provider serverLock sync.Mutex @@ -44,16 +44,22 @@ type Server struct { loggerMiddleware *middlewares.Logger } +type serverEntryPoint struct { + httpServer *manners.GracefulServer + httpRouter *mux.Router +} + // NewServer returns an initialized Server. func NewServer(globalConfiguration GlobalConfiguration) *Server { server := new(Server) + server.serverEntryPoints = make(map[string]serverEntryPoint) server.configurationChan = make(chan types.ConfigMessage, 10) - server.configurationChanValidated = make(chan types.ConfigMessage, 10) - server.sigs = make(chan os.Signal, 1) + server.configurationValidatedChan = make(chan types.ConfigMessage, 10) + server.signals = make(chan os.Signal, 1) server.stopChan = make(chan bool) server.providers = []provider.Provider{} - signal.Notify(server.sigs, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(server.signals, syscall.SIGINT, syscall.SIGTERM) server.currentConfigurations = make(configs) server.globalConfiguration = globalConfiguration server.loggerMiddleware = middlewares.NewLogger(globalConfiguration.AccessLogsFile) @@ -63,38 +69,27 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { // Start starts the server and blocks until server is shutted down. func (server *Server) Start() { - server.configurationRouter = LoadDefaultConfig(server.globalConfiguration) go server.listenProviders() - go server.enableRouter() + go server.listenConfigurations() server.configureProviders() server.startProviders() go server.listenSignals() - - var er error - server.serverLock.Lock() - server.srv, er = server.prepareServer(server.configurationRouter, server.globalConfiguration, nil, server.loggerMiddleware, metrics) - if er != nil { - log.Fatal("Error preparing server: ", er) - } - go server.startServer(server.srv, server.globalConfiguration) - //TODO change that! - time.Sleep(100 * time.Millisecond) - server.serverLock.Unlock() - <-server.stopChan } // Stop stops the server func (server *Server) Stop() { - server.srv.Close() + for _, serverEntryPoint := range server.serverEntryPoints { + serverEntryPoint.httpServer.Close() + } server.stopChan <- true } // Close destroys the server func (server *Server) Close() { close(server.configurationChan) - close(server.configurationChanValidated) - close(server.sigs) + close(server.configurationValidatedChan) + close(server.signals) close(server.stopChan) server.loggerMiddleware.Close() } @@ -104,19 +99,20 @@ func (server *Server) listenProviders() { lastConfigs := make(map[string]*types.ConfigMessage) for { configMsg := <-server.configurationChan - log.Debugf("Configuration receveived from provider %s: %#v", configMsg.ProviderName, configMsg.Configuration) + jsonConf, _ := json.Marshal(configMsg.Configuration) + log.Debugf("Configuration receveived from provider %s: %s", configMsg.ProviderName, string(jsonConf)) lastConfigs[configMsg.ProviderName] = &configMsg if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) { log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration) // last config received more than n s ago - server.configurationChanValidated <- configMsg + server.configurationValidatedChan <- configMsg } else { log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration) go func() { <-time.After(server.globalConfiguration.ProvidersThrottleDuration) if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) { log.Debugf("Waited for %s config, OK", configMsg.ProviderName) - server.configurationChanValidated <- *lastConfigs[configMsg.ProviderName] + server.configurationValidatedChan <- *lastConfigs[configMsg.ProviderName] } }() } @@ -124,9 +120,9 @@ func (server *Server) listenProviders() { } } -func (server *Server) enableRouter() { +func (server *Server) listenConfigurations() { for { - configMsg := <-server.configurationChanValidated + configMsg := <-server.configurationValidatedChan if configMsg.Configuration == nil { log.Info("Skipping empty Configuration") } else if reflect.DeepEqual(server.currentConfigurations[configMsg.ProviderName], configMsg.Configuration) { @@ -139,22 +135,26 @@ func (server *Server) enableRouter() { } newConfigurations[configMsg.ProviderName] = configMsg.Configuration - newConfigurationRouter, err := server.loadConfig(newConfigurations, server.globalConfiguration) + newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration) if err == nil { server.serverLock.Lock() - server.currentConfigurations = newConfigurations - server.configurationRouter = newConfigurationRouter - oldServer := server.srv - newsrv, err := server.prepareServer(server.configurationRouter, server.globalConfiguration, oldServer, server.loggerMiddleware, metrics) - if err != nil { - log.Fatal("Error preparing server: ", err) - } - go server.startServer(newsrv, server.globalConfiguration) - server.srv = newsrv - time.Sleep(1 * time.Second) - if oldServer != nil { - log.Info("Stopping old server") - oldServer.Close() + for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints { + currentServerEntryPoint := server.serverEntryPoints[newServerEntryPointName] + server.currentConfigurations = newConfigurations + currentServerEntryPoint.httpRouter = newServerEntryPoint.httpRouter + oldServer := currentServerEntryPoint.httpServer + newsrv, err := server.prepareServer(currentServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], oldServer, server.loggerMiddleware, metrics) + if err != nil { + log.Fatal("Error preparing server: ", err) + } + go server.startServer(newsrv, server.globalConfiguration) + currentServerEntryPoint.httpServer = newsrv + server.serverEntryPoints[newServerEntryPointName] = currentServerEntryPoint + time.Sleep(1 * time.Second) + if oldServer != nil { + log.Info("Stopping old server") + oldServer.Close() + } } server.serverLock.Unlock() } else { @@ -173,10 +173,6 @@ func (server *Server) configureProviders() { server.providers = append(server.providers, server.globalConfiguration.Marathon) } if server.globalConfiguration.File != nil { - if len(server.globalConfiguration.File.Filename) == 0 { - // no filename, setting to global config file - server.globalConfiguration.File.Filename = viper.GetString("configFile") - } server.providers = append(server.providers, server.globalConfiguration.File) } if server.globalConfiguration.Web != nil { @@ -200,7 +196,8 @@ func (server *Server) configureProviders() { func (server *Server) startProviders() { // start providers for _, provider := range server.providers { - log.Infof("Starting provider %v %+v", reflect.TypeOf(provider), provider) + jsonConf, _ := json.Marshal(provider) + log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf) currentProvider := provider go func() { err := currentProvider.Provide(server.configurationChan) @@ -212,15 +209,18 @@ func (server *Server) startProviders() { } func (server *Server) listenSignals() { - sig := <-server.sigs + sig := <-server.signals log.Infof("I have to go... %+v", sig) log.Info("Stopping server") server.Stop() } // creates a TLS config that allows terminating HTTPS for multiple domains using SNI -func (server *Server) createTLSConfig(certs []Certificate) (*tls.Config, error) { - if len(certs) == 0 { +func (server *Server) createTLSConfig(tlsOption *TLS) (*tls.Config, error) { + if tlsOption == nil { + return nil, nil + } + if len(tlsOption.Certificates) == 0 { return nil, nil } @@ -230,8 +230,8 @@ func (server *Server) createTLSConfig(certs []Certificate) (*tls.Config, error) } var err error - config.Certificates = make([]tls.Certificate, len(certs)) - for i, v := range certs { + config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates)) + for i, v := range tlsOption.Certificates { config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile) if err != nil { return nil, err @@ -244,7 +244,7 @@ func (server *Server) createTLSConfig(certs []Certificate) (*tls.Config, error) } func (server *Server) startServer(srv *manners.GracefulServer, globalConfiguration GlobalConfiguration) { - log.Info("Starting server") + log.Info("Starting server on ", srv.Addr) if srv.TLSConfig != nil { err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig) if err != nil { @@ -259,7 +259,7 @@ func (server *Server) startServer(srv *manners.GracefulServer, globalConfigurati log.Info("Server stopped") } -func (server *Server) prepareServer(router *mux.Router, globalConfiguration GlobalConfiguration, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) { +func (server *Server) prepareServer(router *mux.Router, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) { log.Info("Preparing server") // middlewares var negroni = negroni.New() @@ -267,7 +267,7 @@ func (server *Server) prepareServer(router *mux.Router, globalConfiguration Glob negroni.Use(middleware) } negroni.UseHandler(router) - tlsConfig, err := server.createTLSConfig(globalConfiguration.Certificates) + tlsConfig, err := server.createTLSConfig(entryPoint.TLS) if err != nil { log.Fatalf("Error creating TLS config %s", err) return nil, err @@ -276,13 +276,13 @@ func (server *Server) prepareServer(router *mux.Router, globalConfiguration Glob if oldServer == nil { return manners.NewWithServer( &http.Server{ - Addr: globalConfiguration.Port, + Addr: entryPoint.Address, Handler: negroni, TLSConfig: tlsConfig, }), nil } gracefulServer, err := oldServer.HijackListener(&http.Server{ - Addr: globalConfiguration.Port, + Addr: entryPoint.Address, Handler: negroni, TLSConfig: tlsConfig, }, tlsConfig) @@ -293,80 +293,147 @@ func (server *Server) prepareServer(router *mux.Router, globalConfiguration Glob return gracefulServer, nil } +func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]serverEntryPoint { + serverEntryPoints := make(map[string]serverEntryPoint) + for entryPointName := range globalConfiguration.EntryPoints { + router := server.buildDefaultHTTPRouter() + serverEntryPoints[entryPointName] = serverEntryPoint{ + httpRouter: router, + } + } + return serverEntryPoints +} + // LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic // provider configurations. -func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (*mux.Router, error) { - router := mux.NewRouter() - router.NotFoundHandler = http.HandlerFunc(notFoundHandler) +func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (map[string]serverEntryPoint, error) { + serverEntryPoints := server.buildEntryPoints(globalConfiguration) + redirectHandlers := make(map[string]http.Handler) + backends := map[string]http.Handler{} for _, configuration := range configurations { for frontendName, frontend := range configuration.Frontends { - log.Infof("Creating frontend %s", frontendName) + log.Debugf("Creating frontend %s", frontendName) fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader)) - newRoute := router.NewRoute().Name(frontendName) - for routeName, route := range frontend.Routes { - log.Infof("Creating route %s %s:%s", routeName, route.Rule, route.Value) - newRouteReflect, err := invoke(newRoute, route.Rule, route.Value) - if err != nil { - return nil, err - } - newRoute = newRouteReflect[0].Interface().(*mux.Route) + // default endpoints if not defined in frontends + if len(frontend.EntryPoints) == 0 { + frontend.EntryPoints = globalConfiguration.DefaultEntryPoints } - if backends[frontend.Backend] == nil { - log.Infof("Creating backend %s", frontend.Backend) - var lb http.Handler - rr, _ := roundrobin.New(fwd) - if configuration.Backends[frontend.Backend] == nil { - return nil, errors.New("Backend not found: " + frontend.Backend) + for _, entryPointName := range frontend.EntryPoints { + log.Debugf("Wiring frontend %s to entryPoint %s", frontendName, entryPointName) + if _, ok := serverEntryPoints[entryPointName]; !ok { + return nil, errors.New("Undefined entrypoint: " + entryPointName) } - lbMethod, err := types.NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer) - if err != nil { - configuration.Backends[frontend.Backend].LoadBalancer = &types.LoadBalancer{Method: "wrr"} - } - switch lbMethod { - case types.Drr: - log.Infof("Creating load-balancer drr") - rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger)) - lb = rebalancer - for serverName, server := range configuration.Backends[frontend.Backend].Servers { - url, err := url.Parse(server.URL) - if err != nil { - return nil, err - } - log.Infof("Creating server %s %s", serverName, url.String()) - rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight)) - } - case types.Wrr: - log.Infof("Creating load-balancer wrr") - lb = middlewares.NewWebsocketUpgrader(rr) - for serverName, server := range configuration.Backends[frontend.Backend].Servers { - url, err := url.Parse(server.URL) - if err != nil { - return nil, err - } - log.Infof("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight) - rr.UpsertServer(url, roundrobin.Weight(server.Weight)) + newRoute := serverEntryPoints[entryPointName].httpRouter.NewRoute().Name(frontendName) + for routeName, route := range frontend.Routes { + log.Debugf("Creating route %s %s:%s", routeName, route.Rule, route.Value) + newRouteReflect, err := invoke(newRoute, route.Rule, route.Value) + if err != nil { + return nil, err } + newRoute = newRouteReflect[0].Interface().(*mux.Route) } - var negroni = negroni.New() - if configuration.Backends[frontend.Backend].CircuitBreaker != nil { - log.Infof("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression) - negroni.Use(middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger))) + entryPoint := globalConfiguration.EntryPoints[entryPointName] + if entryPoint.Redirect != nil { + if redirectHandlers[entryPointName] != nil { + newRoute.Handler(redirectHandlers[entryPointName]) + } else if handler, err := server.loadEntryPointConfig(entryPointName, entryPoint); err != nil { + return nil, err + } else { + newRoute.Handler(handler) + redirectHandlers[entryPointName] = handler + } } else { - negroni.UseHandler(lb) + if backends[frontend.Backend] == nil { + log.Debugf("Creating backend %s", frontend.Backend) + var lb http.Handler + rr, _ := roundrobin.New(fwd) + if configuration.Backends[frontend.Backend] == nil { + return nil, errors.New("Undefined backend: " + frontend.Backend) + } + lbMethod, err := types.NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer) + if err != nil { + configuration.Backends[frontend.Backend].LoadBalancer = &types.LoadBalancer{Method: "wrr"} + } + switch lbMethod { + case types.Drr: + log.Debugf("Creating load-balancer drr") + rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger)) + lb = rebalancer + for serverName, server := range configuration.Backends[frontend.Backend].Servers { + url, err := url.Parse(server.URL) + if err != nil { + return nil, err + } + log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight) + rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight)) + } + case types.Wrr: + log.Debugf("Creating load-balancer wrr") + lb = middlewares.NewWebsocketUpgrader(rr) + for serverName, server := range configuration.Backends[frontend.Backend].Servers { + url, err := url.Parse(server.URL) + if err != nil { + return nil, err + } + log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight) + rr.UpsertServer(url, roundrobin.Weight(server.Weight)) + } + } + var negroni = negroni.New() + if configuration.Backends[frontend.Backend].CircuitBreaker != nil { + log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression) + negroni.Use(middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger))) + } else { + negroni.UseHandler(lb) + } + backends[frontend.Backend] = negroni + } else { + log.Debugf("Reusing backend %s", frontend.Backend) + } + newRoute.Handler(backends[frontend.Backend]) + } + err := newRoute.GetError() + if err != nil { + log.Errorf("Error building route: %s", err) } - backends[frontend.Backend] = negroni - } else { - log.Infof("Reusing backend %s", frontend.Backend) - } - // stream.New(backends[frontend.Backend], stream.Retry("IsNetworkError() && Attempts() <= " + strconv.Itoa(globalConfiguration.Replay)), stream.Logger(oxyLogger)) - - newRoute.Handler(backends[frontend.Backend]) - err := newRoute.GetError() - if err != nil { - log.Errorf("Error building route: %s", err) } } } - return router, nil + return serverEntryPoints, nil +} + +func (server *Server) loadEntryPointConfig(entryPointName string, entryPoint *EntryPoint) (http.Handler, error) { + regex := entryPoint.Redirect.Regex + replacement := entryPoint.Redirect.Replacement + if len(entryPoint.Redirect.EntryPoint) > 0 { + regex = "^(?:https?:\\/\\/)?([\\da-z\\.-]+)(?::\\d+)(.*)$" + if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint] == nil { + return nil, errors.New("Unkown entrypoint " + entryPoint.Redirect.EntryPoint) + } + protocol := "http" + if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].TLS != nil { + protocol = "https" + } + r, _ := regexp.Compile("(:\\d+)") + match := r.FindStringSubmatch(server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) + if len(match) == 0 { + return nil, errors.New("Bad Address format: " + server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) + } + replacement = protocol + "://$1" + match[0] + "$2" + } + rewrite, err := middlewares.NewRewrite(regex, replacement, true) + if err != nil { + return nil, err + } + log.Debugf("Creating entryPoint redirect %s -> %s : %s -> %s", entryPointName, entryPoint.Redirect.EntryPoint, regex, replacement) + negroni := negroni.New() + negroni.Use(rewrite) + return negroni, nil +} + +func (server *Server) buildDefaultHTTPRouter() *mux.Router { + router := mux.NewRouter() + router.NotFoundHandler = http.HandlerFunc(notFoundHandler) + return router } diff --git a/types/types.go b/types/types.go index 22abb8dd2..30bfbf483 100644 --- a/types/types.go +++ b/types/types.go @@ -36,6 +36,7 @@ type Route struct { // Frontend holds frontend configuration. type Frontend struct { + EntryPoints []string `json:"entryPoints,omitempty"` Backend string `json:"backend,omitempty"` Routes map[string]Route `json:"routes,omitempty"` PassHostHeader bool `json:"passHostHeader,omitempty"` diff --git a/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html b/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html index c5f3970be..48c8e67ba 100644 --- a/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html +++ b/webui/src/app/sections/providers/frontend-monitor/frontend-monitor.html @@ -17,6 +17,7 @@ From 81cb00573f8c376de9b6e1a58ca548882f3ab2a2 Mon Sep 17 00:00:00 2001 From: emile Date: Mon, 1 Feb 2016 11:07:05 +0100 Subject: [PATCH 2/4] Fix tests to accept entrypoints --- integration/basic_test.go | 7 ++-- integration/consul_test.go | 8 +++-- integration/fixtures/consul/simple.toml | 15 ++++----- integration/fixtures/docker/simple.toml | 15 ++++----- .../fixtures/file/56-simple-panic.toml | 15 ++++----- integration/fixtures/file/simple.toml | 15 ++++----- integration/fixtures/https/https_sni.toml | 18 ++++++---- integration/fixtures/marathon/simple.toml | 15 ++++----- integration/fixtures/simple_default.toml | 14 +++----- integration/fixtures/simple_web.toml | 7 ++-- integration/marathon_test.go | 8 +++-- provider/docker_test.go | 12 ++++--- provider/kv_test.go | 4 +-- provider/marathon_test.go | 33 ++++++++++++++++++- 14 files changed, 107 insertions(+), 79 deletions(-) diff --git a/integration/basic_test.go b/integration/basic_test.go index d9a9178a8..90dd2fd25 100644 --- a/integration/basic_test.go +++ b/integration/basic_test.go @@ -46,9 +46,10 @@ func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) { // TODO validate : run on 80 resp, err := http.Get("http://127.0.0.1:8000/") - // Expected a 404 as we did not configure anything - c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, 404) + // Expected no response as we did not configure anything + c.Assert(resp, checker.IsNil) + c.Assert(err, checker.NotNil) + c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused")) } func (s *SimpleSuite) TestWithWebConfig(c *check.C) { diff --git a/integration/consul_test.go b/integration/consul_test.go index 07af1aa7f..c672cfde2 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -5,6 +5,7 @@ import ( "os/exec" "time" + "fmt" checker "github.com/vdemeester/shakers" check "gopkg.in/check.v1" ) @@ -19,7 +20,8 @@ func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) { // TODO validate : run on 80 resp, err := http.Get("http://127.0.0.1:8000/") - // Expected a 404 as we did not comfigure anything - c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, 404) + // Expected no response as we did not configure anything + c.Assert(resp, checker.IsNil) + c.Assert(err, checker.NotNil) + c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused")) } diff --git a/integration/fixtures/consul/simple.toml b/integration/fixtures/consul/simple.toml index 34947f7e8..287738af7 100644 --- a/integration/fixtures/consul/simple.toml +++ b/integration/fixtures/consul/simple.toml @@ -1,12 +1,9 @@ -# Reverse proxy port -# -# Optional -# Default: ":80" -# -# port = ":80" -port = ":8000" -# -# LogLevel +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + logLevel = "DEBUG" [consul] diff --git a/integration/fixtures/docker/simple.toml b/integration/fixtures/docker/simple.toml index 963cfd5a8..d055def10 100644 --- a/integration/fixtures/docker/simple.toml +++ b/integration/fixtures/docker/simple.toml @@ -1,12 +1,9 @@ -# Reverse proxy port -# -# Optional -# Default: ":80" -# -# port = ":80" -port = ":8000" -# -# LogLevel +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + logLevel = "DEBUG" [docker] diff --git a/integration/fixtures/file/56-simple-panic.toml b/integration/fixtures/file/56-simple-panic.toml index f8c792e65..278b9cb9c 100644 --- a/integration/fixtures/file/56-simple-panic.toml +++ b/integration/fixtures/file/56-simple-panic.toml @@ -1,12 +1,9 @@ -# Reverse proxy port -# -# Optional -# Default: ":80" -# -# port = ":80" -port = ":8000" -# -# LogLevel +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + logLevel = "DEBUG" [file] diff --git a/integration/fixtures/file/simple.toml b/integration/fixtures/file/simple.toml index 3db190f40..bcdc07c6e 100644 --- a/integration/fixtures/file/simple.toml +++ b/integration/fixtures/file/simple.toml @@ -1,12 +1,9 @@ -# Reverse proxy port -# -# Optional -# Default: ":80" -# -# port = ":80" -port = ":8000" -# -# LogLevel +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + logLevel = "DEBUG" [file] diff --git a/integration/fixtures/https/https_sni.toml b/integration/fixtures/https/https_sni.toml index aebf22657..2a1fa04a9 100644 --- a/integration/fixtures/https/https_sni.toml +++ b/integration/fixtures/https/https_sni.toml @@ -1,13 +1,17 @@ -port = ":4443" logLevel = "DEBUG" -[[certificates]] -CertFile = "fixtures/https/snitest.com.cert" -KeyFile = "fixtures/https/snitest.com.key" +defaultEntryPoints = ["https"] -[[certificates]] -CertFile = "fixtures/https/snitest.org.cert" -KeyFile = "fixtures/https/snitest.org.key" +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = "fixtures/https/snitest.com.cert" + KeyFile = "fixtures/https/snitest.com.key" + [[entryPoints.https.tls.certificates]] + CertFile = "fixtures/https/snitest.org.cert" + KeyFile = "fixtures/https/snitest.org.key" [file] diff --git a/integration/fixtures/marathon/simple.toml b/integration/fixtures/marathon/simple.toml index c6cfbb05a..fedfdfb3d 100644 --- a/integration/fixtures/marathon/simple.toml +++ b/integration/fixtures/marathon/simple.toml @@ -1,12 +1,9 @@ -# Reverse proxy port -# -# Optional -# Default: ":80" -# -# port = ":80" -port = ":8000" -# -# LogLevel +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + logLevel = "DEBUG" [marathon] diff --git a/integration/fixtures/simple_default.toml b/integration/fixtures/simple_default.toml index 3f5fe41df..d61410d7c 100644 --- a/integration/fixtures/simple_default.toml +++ b/integration/fixtures/simple_default.toml @@ -1,9 +1,5 @@ -# Reverse proxy port -# -# Optional -# Default: ":80" -# -port = ":8000" -# -# LogLevel -logLevel = "DEBUG" +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" \ No newline at end of file diff --git a/integration/fixtures/simple_web.toml b/integration/fixtures/simple_web.toml index 49452f6e5..b05590cf2 100644 --- a/integration/fixtures/simple_web.toml +++ b/integration/fixtures/simple_web.toml @@ -1,6 +1,9 @@ -port = ":8000" logLevel = "DEBUG" +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" [web] - address = ":8080" diff --git a/integration/marathon_test.go b/integration/marathon_test.go index 40a42ffd6..e13b25bbf 100644 --- a/integration/marathon_test.go +++ b/integration/marathon_test.go @@ -5,6 +5,7 @@ import ( "os/exec" "time" + "fmt" checker "github.com/vdemeester/shakers" check "gopkg.in/check.v1" ) @@ -19,7 +20,8 @@ func (s *MarathonSuite) TestSimpleConfiguration(c *check.C) { // TODO validate : run on 80 resp, err := http.Get("http://127.0.0.1:8000/") - // Expected a 404 as we did not configure anything - c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, 404) + // Expected no response as we did not configure anything + c.Assert(resp, checker.IsNil) + c.Assert(err, checker.NotNil) + c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused")) } diff --git a/provider/docker_test.go b/provider/docker_test.go index 54b30dc5d..590c16ccd 100644 --- a/provider/docker_test.go +++ b/provider/docker_test.go @@ -682,7 +682,8 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, expectedFrontends: map[string]*types.Frontend{ `"frontend-Host-test-docker-localhost"`: { - Backend: "backend-test", + Backend: "backend-test", + EntryPoints: []string{}, Routes: map[string]types.Route{ `"route-frontend-Host-test-docker-localhost"`: { Rule: "Host", @@ -709,7 +710,8 @@ func TestDockerLoadDockerConfig(t *testing.T) { Name: "test1", Config: &docker.Config{ Labels: map[string]string{ - "traefik.backend": "foobar", + "traefik.backend": "foobar", + "traefik.frontend.entryPoints": "http,https", }, }, NetworkSettings: &docker.NetworkSettings{ @@ -736,7 +738,8 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, expectedFrontends: map[string]*types.Frontend{ `"frontend-Host-test1-docker-localhost"`: { - Backend: "backend-foobar", + Backend: "backend-foobar", + EntryPoints: []string{"http", "https"}, Routes: map[string]types.Route{ `"route-frontend-Host-test1-docker-localhost"`: { Rule: "Host", @@ -745,7 +748,8 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, }, `"frontend-Host-test2-docker-localhost"`: { - Backend: "backend-foobar", + Backend: "backend-foobar", + EntryPoints: []string{}, Routes: map[string]types.Route{ `"route-frontend-Host-test2-docker-localhost"`: { Rule: "Host", diff --git a/provider/kv_test.go b/provider/kv_test.go index 3cb79f257..d879ccfa4 100644 --- a/provider/kv_test.go +++ b/provider/kv_test.go @@ -176,7 +176,7 @@ func TestKvGet(t *testing.T) { } for _, c := range cases { - actual := c.provider.get(c.keys...) + actual := c.provider.get("", c.keys...) if actual != c.expected { t.Fatalf("expected %v, got %v for %v and %v", c.expected, actual, c.keys, c.provider) } @@ -188,7 +188,7 @@ func TestKvGet(t *testing.T) { Error: true, }, } - actual := provider.get("anything") + actual := provider.get("", "anything") if actual != "" { t.Fatalf("Should have return nil, got %v", actual) } diff --git a/provider/marathon_test.go b/provider/marathon_test.go index 8ffc026eb..642f28224 100644 --- a/provider/marathon_test.go +++ b/provider/marathon_test.go @@ -84,7 +84,8 @@ func TestMarathonLoadConfig(t *testing.T) { }, expectedFrontends: map[string]*types.Frontend{ `frontend-test`: { - Backend: "backend-test", + Backend: "backend-test", + EntryPoints: []string{}, Routes: map[string]types.Route{ `route-host-test`: { Rule: "Host", @@ -735,6 +736,36 @@ func TestMarathonGetPassHostHeader(t *testing.T) { } } +func TestMarathonGetEntryPoints(t *testing.T) { + provider := &Marathon{} + + applications := []struct { + application marathon.Application + expected []string + }{ + { + application: marathon.Application{}, + expected: []string{}, + }, + { + application: marathon.Application{ + Labels: map[string]string{ + "traefik.frontend.entryPoints": "http,https", + }, + }, + expected: []string{"http", "https"}, + }, + } + + for _, a := range applications { + actual := provider.getEntryPoints(a.application) + + if !reflect.DeepEqual(actual, a.expected) { + t.Fatalf("expected %#v, got %#v", a.expected, actual) + } + } +} + func TestMarathonGetFrontendValue(t *testing.T) { provider := &Marathon{ Domain: "docker.localhost", From a8cc26fd91b7b1fb06ee44f74f74b72be05bddb7 Mon Sep 17 00:00:00 2001 From: emile Date: Mon, 1 Feb 2016 16:08:58 +0100 Subject: [PATCH 3/4] Add entrypoints to providers --- cmd.go | 2 +- configuration.go | 2 +- integration/marathon_test.go | 2 +- provider/docker.go | 8 ++++++++ provider/kv.go | 31 +++++++++++++++++++++++-------- provider/marathon.go | 9 +++++++++ templates/docker.tmpl | 3 +++ templates/kv.tmpl | 20 ++++++++++++-------- templates/marathon.tmpl | 3 +++ tests/consul-config.sh | 2 ++ 10 files changed, 63 insertions(+), 19 deletions(-) diff --git a/cmd.go b/cmd.go index b39a1e6f1..b97406fa3 100644 --- a/cmd.go +++ b/cmd.go @@ -4,12 +4,12 @@ Copyright package main import ( + "encoding/json" fmtlog "log" "os" "strings" "time" - "encoding/json" log "github.com/Sirupsen/logrus" "github.com/emilevauge/traefik/middlewares" "github.com/emilevauge/traefik/provider" diff --git a/configuration.go b/configuration.go index e02a1fbb5..facd3e8f4 100644 --- a/configuration.go +++ b/configuration.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" fmtlog "log" + "regexp" "strings" "time" @@ -11,7 +12,6 @@ import ( "github.com/emilevauge/traefik/types" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" - "regexp" ) // GlobalConfiguration holds global configuration (with providers, etc.). diff --git a/integration/marathon_test.go b/integration/marathon_test.go index e13b25bbf..ea45381f4 100644 --- a/integration/marathon_test.go +++ b/integration/marathon_test.go @@ -1,11 +1,11 @@ package main import ( + "fmt" "net/http" "os/exec" "time" - "fmt" checker "github.com/vdemeester/shakers" check "gopkg.in/check.v1" ) diff --git a/provider/docker.go b/provider/docker.go index e4e6a1f5b..68ab5f958 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -107,6 +107,7 @@ func (provider *Docker) loadDockerConfig(containersInspected []docker.Container) "getDomain": provider.getDomain, "getProtocol": provider.getProtocol, "getPassHostHeader": provider.getPassHostHeader, + "getEntryPoints": provider.getEntryPoints, "getFrontendValue": provider.getFrontendValue, "getFrontendRule": provider.getFrontendRule, "replace": replace, @@ -234,6 +235,13 @@ func (provider *Docker) getPassHostHeader(container docker.Container) string { return "false" } +func (provider *Docker) getEntryPoints(container docker.Container) []string { + if entryPoints, err := getLabel(container, "traefik.frontend.entryPoints"); err == nil { + return strings.Split(entryPoints, ",") + } + return []string{} +} + func getLabel(container docker.Container, label string) (string, error) { for key, value := range container.Config.Labels { if key == label { diff --git a/provider/kv.go b/provider/kv.go index 4fc5cb143..c8c674721 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -73,9 +73,10 @@ func (provider *Kv) loadConfig() *types.Configuration { provider.Prefix, } var KvFuncMap = template.FuncMap{ - "List": provider.list, - "Get": provider.get, - "Last": provider.last, + "List": provider.list, + "Get": provider.get, + "SplitGet": provider.splitGet, + "Last": provider.last, } configuration, err := provider.getConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) @@ -89,7 +90,7 @@ func (provider *Kv) list(keys ...string) []string { joinedKeys := strings.Join(keys, "") keysPairs, err := provider.kvclient.List(joinedKeys) if err != nil { - log.Error("Error getting keys: ", joinedKeys, err) + log.Errorf("Error getting keys %s %s ", joinedKeys, err) return nil } directoryKeys := make(map[string]string) @@ -100,18 +101,32 @@ func (provider *Kv) list(keys ...string) []string { return fun.Values(directoryKeys).([]string) } -func (provider *Kv) get(keys ...string) string { +func (provider *Kv) get(defaultValue string, keys ...string) string { joinedKeys := strings.Join(keys, "") keyPair, err := provider.kvclient.Get(joinedKeys) if err != nil { - log.Error("Error getting key: ", joinedKeys, err) - return "" + log.Warnf("Error getting key %s %s, setting default %s", joinedKeys, err, defaultValue) + return defaultValue } else if keyPair == nil { - return "" + log.Warnf("Error getting key %s, setting default %s", joinedKeys, defaultValue) + return defaultValue } return string(keyPair.Value) } +func (provider *Kv) splitGet(keys ...string) []string { + joinedKeys := strings.Join(keys, "") + keyPair, err := provider.kvclient.Get(joinedKeys) + if err != nil { + log.Warnf("Error getting key %s %s, setting default empty", joinedKeys, err) + return []string{} + } else if keyPair == nil { + log.Warnf("Error getting key %s, setting default %empty", joinedKeys) + return []string{} + } + return strings.Split(string(keyPair.Value), ",") +} + func (provider *Kv) last(key string) string { splittedKey := strings.Split(key, "/") return splittedKey[len(splittedKey)-1] diff --git a/provider/marathon.go b/provider/marathon.go index 7cc04c483..26982e460 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -4,6 +4,7 @@ import ( "errors" "net/url" "strconv" + "strings" "text/template" "github.com/BurntSushi/ty/fun" @@ -86,6 +87,7 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration { "getDomain": provider.getDomain, "getProtocol": provider.getProtocol, "getPassHostHeader": provider.getPassHostHeader, + "getEntryPoints": provider.getEntryPoints, "getFrontendValue": provider.getFrontendValue, "getFrontendRule": provider.getFrontendRule, "replace": replace, @@ -286,6 +288,13 @@ func (provider *Marathon) getPassHostHeader(application marathon.Application) st return "false" } +func (provider *Marathon) getEntryPoints(application marathon.Application) []string { + if entryPoints, err := provider.getLabel(application, "traefik.frontend.entryPoints"); err == nil { + return strings.Split(entryPoints, ",") + } + return []string{} +} + // getFrontendValue returns the frontend value for the specified application, using // it's label. It returns a default one if the label is not present. func (provider *Marathon) getFrontendValue(application marathon.Application) string { diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 89b79f90a..afbee282c 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -8,6 +8,9 @@ [frontends."frontend-{{$frontend}}"]{{$container := index $containers 0}} backend = "backend-{{getBackend $container}}" passHostHeader = {{getPassHostHeader $container}} + entryPoints = [{{range getEntryPoints $container}} + "{{.}}", + {{end}}] [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] rule = "{{getFrontendRule $container}}" value = "{{getFrontendValue $container}}" diff --git a/templates/kv.tmpl b/templates/kv.tmpl index f5bb470ac..9fd770d24 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -5,13 +5,13 @@ {{$backend := .}} {{$servers := List $backend "/servers/" }} -{{$circuitBreaker := Get . "/circuitbreaker/" "expression"}} +{{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}} {{with $circuitBreaker}} [backends.{{Last $backend}}.circuitBreaker] expression = "{{$circuitBreaker}}" {{end}} -{{$loadBalancer := Get . "/loadbalancer/" "method"}} +{{$loadBalancer := Get "" . "/loadbalancer/" "method"}} {{with $loadBalancer}} [backends.{{Last $backend}}.loadBalancer] method = "{{$loadBalancer}}" @@ -19,20 +19,24 @@ {{range $servers}} [backends.{{Last $backend}}.servers.{{Last .}}] - url = "{{Get . "/url"}}" - weight = {{Get . "/weight"}} + url = "{{Get "" . "/url"}}" + weight = {{Get "" . "/weight"}} {{end}} {{end}} [frontends]{{range $frontends}} {{$frontend := Last .}} + {{$entryPoints := SplitGet . "/entrypoints"}} [frontends.{{$frontend}}] - backend = "{{Get . "/backend"}}" - passHostHeader = {{Get . "/passHostHeader"}} + backend = "{{Get "" . "/backend"}}" + passHostHeader = {{Get "false" . "/passHostHeader"}} + entryPoints = [{{range $entryPoints}} + "{{.}}", + {{end}}] {{$routes := List . "/routes/"}} {{range $routes}} [frontends.{{$frontend}}.routes.{{Last .}}] - rule = "{{Get . "/rule"}}" - value = "{{Get . "/value"}}" + rule = "{{Get "" . "/rule"}}" + value = "{{Get "" . "/value"}}" {{end}} {{end}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index ae31e9f8f..5558924fd 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -9,6 +9,9 @@ [frontends.frontend{{.ID | replace "/" "-"}}] backend = "backend{{getBackend .}}" passHostHeader = {{getPassHostHeader .}} + entryPoints = [{{range getEntryPoints .}} + "{{.}}", + {{end}}] [frontends.frontend{{.ID | replace "/" "-"}}.routes.route-host{{.ID | replace "/" "-"}}] rule = "{{getFrontendRule .}}" value = "{{getFrontendValue .}}" diff --git a/tests/consul-config.sh b/tests/consul-config.sh index e567f3319..58952031e 100755 --- a/tests/consul-config.sh +++ b/tests/consul-config.sh @@ -16,10 +16,12 @@ curl -i -H "Accept: application/json" -X PUT -d "2" ht # frontend 1 curl -i -H "Accept: application/json" -X PUT -d "backend2" http://localhost:8500/v1/kv/traefik/frontends/frontend1/backend +curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend1/entrypoints curl -i -H "Accept: application/json" -X PUT -d "Host" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule curl -i -H "Accept: application/json" -X PUT -d "test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/value # frontend 2 curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/traefik/frontends/frontend2/backend +curl -i -H "Accept: application/json" -X PUT -d "http,https" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints curl -i -H "Accept: application/json" -X PUT -d "Path" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule curl -i -H "Accept: application/json" -X PUT -d "/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/value From 4152bd5e26130ccc70fc631eb1c66b8b83b3e18b Mon Sep 17 00:00:00 2001 From: emile Date: Mon, 1 Feb 2016 16:09:13 +0100 Subject: [PATCH 4/4] Update doc with entrypoints --- README.md | 20 ++++++-- docs/img/apollo-logo.png | Bin 0 -> 7417 bytes docs/img/mantl-logo.png | Bin 0 -> 19533 bytes docs/index.md | 107 +++++++++++++++++++++++++++++++-------- traefik.sample.toml | 56 +++++++++++++++----- 5 files changed, 147 insertions(+), 36 deletions(-) create mode 100644 docs/img/apollo-logo.png create mode 100644 docs/img/mantl-logo.png diff --git a/README.md b/README.md index d58ab6e46..05da4a276 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ![Træfɪk](http://traefik.github.io/traefik.logo.svg "Træfɪk") ___ -[![Circle CI](https://circleci.com/gh/emilevauge/traefik.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/emilevauge/traefik) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/EmileVauge/traefik/blob/master/LICENSE.md) +[![Circle CI](https://circleci.com/gh/emilevauge/traefik/tree/master.png?circle-token)](https://circleci.com/gh/emilevauge/traefik) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/EmileVauge/traefik/blob/master/LICENSE.md) [![Join the chat at https://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com) @@ -22,7 +22,7 @@ It supports several backends ([Docker :whale:](https://www.docker.com/), [Mesos/ - Circuit breakers on backends - Round Robin, rebalancer load-balancers - Rest Metrics -- Tiny docker image included +- Tiny docker image included [![Image Layers](https://badge.imagelayers.io/emilevauge/traefik:latest.svg)](https://imagelayers.io/?images=emilevauge/traefik:latest 'Image Layers') - SSL backends support - SSL frontend support - Clean AngularJS Web UI @@ -76,6 +76,20 @@ You can find the complete documentation [here](docs/index.md). Refer to the [benchmarks section](docs/index.md#benchmarks) in the documentation. +## Træfɪk here and there + +These projects use Træfɪk internally. If your company uses Træfɪk, we would be glad to get your feedback :) Contact us on [![Join the chat at https://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com) + +- Project [Mantl](http://http://mantl.io/) from Cisco + +![Web UI Providers](docs/img/mantl-logo.png) +> Mantl is a modern platform for rapidly deploying globally distributed services. A container orchestrator, docker, a network stack, something to pool your logs, something to monitor health, a sprinkle of service discovery and some automation. + +- Project [Apollo](http://capgemini.github.io/devops/apollo/) from Cap Gemini + +![Web UI Providers](docs/img/apollo-logo.png) +> Apollo is an open source project to aid with building and deploying IAAS and PAAS services. It is particularly geared towards managing containerized applications across multiple hosts, and big data type workloads. Apollo leverages other open source components to provide basic mechanisms for deployment, maintenance, and scaling of infrastructure and applications. + ## Contributing ### Building diff --git a/docs/img/apollo-logo.png b/docs/img/apollo-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2fc9659981885cb1708f19ecc9991ae44ca94b79 GIT binary patch literal 7417 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 TRR%bn0k+3uulvuenC$-opA*$Y literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index a4239409d..0ec4ebc92 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,8 +39,7 @@ Frontends can be defined using the following rules: A frontend is a set of rules that forwards the incoming http traffic to a backend. - You can optionally enable `passHostHeader` to -- []forward client `Host` header to the backend. + You can optionally enable `passHostHeader` to forward client `Host` header to the backend. ### HTTP Backends @@ -103,13 +102,13 @@ Flags: --boltdb.filename string Override default configuration template. For advanced users :) --boltdb.prefix string Prefix used for KV store (default "/traefik") --boltdb.watch Watch provider (default true) - --certificates value SSL certificates and keys pair, ie 'tests/traefik.crt,tests/traefik.key'. You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI -c, --configFile string Configuration file to use (TOML, JSON, YAML, HCL). --consul Enable Consul backend --consul.endpoint string Consul server endpoint (default "127.0.0.1:8500") --consul.filename string Override default configuration template. For advanced users :) --consul.prefix string Prefix used for KV store (default "/traefik") --consul.watch Watch provider (default true) + --defaultEntryPoints value Entrypoints to be used by frontends that do not specify any entrypoint (default &main.DefaultEntryPoints(nil)) --docker Enable Docker backend --docker.domain string Default domain used --docker.endpoint string Docker server endpoint. Can be a tcp or a unix socket endpoint (default "unix:///var/run/docker.sock") @@ -120,6 +119,7 @@ Flags: --docker.tls.insecureSkipVerify TLS insecure skip verify --docker.tls.key string TLS key --docker.watch Watch provider (default true) + --entryPoints value Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key' --etcd Enable Etcd backend --etcd.endpoint string Etcd server endpoint (default "127.0.0.1:4001") --etcd.filename string Override default configuration template. For advanced users :) @@ -162,12 +162,45 @@ Use "traefik [command] --help" for more information about a command. # Global configuration ################################################################ -# Reverse proxy port +# Entrypoints definition # # Optional -# Default: ":80" +# Default: +# [entryPoints] +# [entryPoints.http] +# address = ":80" # -# port = ":80" +# To redirect an http entrypoint to an https entrypoint (with SNI support): +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.redirect] +# entryPoint = "https" +# [entryPoints.https] +# address = ":443" +# [entryPoints.https.tls] +# [[entryPoints.https.tls.certificates]] +# CertFile = "integration/fixtures/https/snitest.com.cert" +# KeyFile = "integration/fixtures/https/snitest.com.key" +# [[entryPoints.https.tls.certificates]] +# CertFile = "integration/fixtures/https/snitest.org.cert" +# KeyFile = "integration/fixtures/https/snitest.org.key" +# +# To redirect an entrypoint rewriting the URL: +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.redirect] +# regex = "^http://localhost/(.*)" +# replacement = "http://mydomain/$1" + +# Entrypoints to be used by frontends that do not specify any entrypoint. +# Each frontend can specify its own entrypoints. +# +# Optional +# Default: ["http"] +# +# defaultEntryPoints = ["http", "https"] # Timeout in seconds. # Duration to give active requests a chance to finish during hot-reloads @@ -197,15 +230,6 @@ Use "traefik [command] --help" for more information about a command. # # logLevel = "ERROR" -# SSL certificates and keys -# You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI -# -# Optional -# -# [[certificates]] -# CertFile = "traefik.crt" -# KeyFile = "traefik.key" - # Backends throttle duration: minimum duration between 2 events from providers # before applying a new configuration. It avoids unnecessary reloads if multiples events # are sent in a short amount of time. @@ -234,7 +258,21 @@ Like any other reverse proxy, Træfɪk can be configured with a file. You have t ```toml # traefik.toml -port = ":80" +defaultEntryPoints = ["http", "https"] +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = "integration/fixtures/https/snitest.com.cert" + KeyFile = "integration/fixtures/https/snitest.com.key" + [[entryPoints.https.tls.certificates]] + CertFile = "integration/fixtures/https/snitest.org.cert" + KeyFile = "integration/fixtures/https/snitest.org.key" graceTimeOut = 10 logLevel = "DEBUG" @@ -270,7 +308,13 @@ logLevel = "DEBUG" [frontends.frontend2] backend = "backend1" passHostHeader = true - [frontends.frontend2.routes.test_2] + entrypoints = ["https"] # overrides defaultEntryPoints + [frontends.frontend2.routes.test_1] + rule = "Host" + value = "{subdomain:[a-z]+}.localhost" + [frontends.frontend3] + entrypoints = ["http", "https"] # overrides defaultEntryPoints + backend = "backend2" rule = "Path" value = "/test" ``` @@ -279,7 +323,20 @@ logLevel = "DEBUG" ```toml # traefik.toml -port = ":80" +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = "integration/fixtures/https/snitest.com.cert" + KeyFile = "integration/fixtures/https/snitest.com.key" + [[entryPoints.https.tls.certificates]] + CertFile = "integration/fixtures/https/snitest.org.cert" + KeyFile = "integration/fixtures/https/snitest.org.key" graceTimeOut = 10 logLevel = "DEBUG" @@ -318,10 +375,15 @@ filename = "rules.toml" [frontends.frontend2] backend = "backend1" passHostHeader = true - [frontends.frontend2.routes.test_2] + entrypoints = ["https"] # overrides defaultEntryPoints + [frontends.frontend2.routes.test_1] + rule = "Host" + value = "{subdomain:[a-z]+}.localhost" + [frontends.frontend3] + entrypoints = ["http", "https"] # overrides defaultEntryPoints + backend = "backend2" rule = "Path" value = "/test" - ``` If you want Træfɪk to watch file changes automatically, just add: @@ -534,6 +596,7 @@ Labels can be used on containers to override default behaviour: - `traefik.frontend.rule=Host`: override the default frontend rule (Default: Host). See [frontends](#frontends). - `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{containerName}.{domain}`) See [frontends](#frontends). Must be associated with label traefik.frontend.rule. - `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend. +- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. * `traefik.domain=traefik.localhost`: override the default domain @@ -598,6 +661,7 @@ Labels can be used on containers to override default behaviour: - `traefik.frontend.rule=Host`: override the default frontend rule (Default: Host). See [frontends](#frontends). - `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{appName}.{domain}`) See [frontends](#frontends). Must be associated with label traefik.frontend.rule. - `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend. +- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. * `traefik.domain=traefik.localhost`: override the default domain ## Consul backend @@ -676,6 +740,7 @@ The Keys-Values structure should look (using `prefix = "/traefik"`): |----------------------------------------------------|------------| | `/traefik/frontends/frontend2/backend` | `backend1` | | `/traefik/frontends/frontend2/passHostHeader` | `true` | +| `/traefik/frontends/frontend2/entrypoints` |`http,https`| | `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` | | `/traefik/frontends/frontend2/routes/test_2/value` | `/test` | @@ -756,6 +821,7 @@ The Keys-Values structure should look (using `prefix = "/traefik"`): |----------------------------------------------------|------------| | `/traefik/frontends/frontend2/backend` | `backend1` | | `/traefik/frontends/frontend2/passHostHeader` | `true` | +| `/traefik/frontends/frontend2/entrypoints` |`http,https`| | `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` | | `/traefik/frontends/frontend2/routes/test_2/value` | `/test` | @@ -835,6 +901,7 @@ The Keys-Values structure should look (using `prefix = "/traefik"`): |----------------------------------------------------|------------| | `/traefik/frontends/frontend2/backend` | `backend1` | | `/traefik/frontends/frontend2/passHostHeader` | `true` | +| `/traefik/frontends/frontend2/entrypoints` |`http,https`| | `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` | | `/traefik/frontends/frontend2/routes/test_2/value` | `/test` | diff --git a/traefik.sample.toml b/traefik.sample.toml index 7d120959b..3f77ea863 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -2,12 +2,45 @@ # Global configuration ################################################################ -# Reverse proxy port +# Entrypoints definition # # Optional -# Default: ":80" +# Default: +# [entryPoints] +# [entryPoints.http] +# address = ":80" # -# port = ":80" +# To redirect an http entrypoint to an https entrypoint (with SNI support): +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.redirect] +# entryPoint = "https" +# [entryPoints.https] +# address = ":443" +# [entryPoints.https.tls] +# [[entryPoints.https.tls.certificates]] +# CertFile = "integration/fixtures/https/snitest.com.cert" +# KeyFile = "integration/fixtures/https/snitest.com.key" +# [[entryPoints.https.tls.certificates]] +# CertFile = "integration/fixtures/https/snitest.org.cert" +# KeyFile = "integration/fixtures/https/snitest.org.key" +# +# To redirect an entrypoint rewriting the URL: +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.redirect] +# regex = "^http://localhost/(.*)" +# replacement = "http://mydomain/$1" + +# Entrypoints to be used by frontends that do not specify any entrypoint. +# Each frontend can specify its own entrypoints. +# +# Optional +# Default: ["http"] +# +# defaultEntryPoints = ["http", "https"] # Timeout in seconds. # Duration to give active requests a chance to finish during hot-reloads @@ -37,15 +70,6 @@ # # logLevel = "ERROR" -# SSL certificates and keys -# You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI -# -# Optional -# -# [[certificates]] -# CertFile = "traefik.crt" -# KeyFile = "traefik.key" - # Backends throttle duration: minimum duration between 2 events from providers # before applying a new configuration. It avoids unnecessary reloads if multiples events # are sent in a short amount of time. @@ -390,6 +414,12 @@ # [frontends.frontend2] # backend = "backend1" # passHostHeader = true -# [frontends.frontend2.routes.test_2] +# entrypoints = ["https"] # overrides defaultEntryPoints +# [frontends.frontend2.routes.test_1] +# rule = "Host" +# value = "{subdomain:[a-z]+}.localhost" +# [frontends.frontend3] +# entrypoints = ["http", "https"] # overrides defaultEntryPoints +# backend = "backend2" # rule = "Path" # value = "/test"