Add multiple entry points support, add entry point redirection

This commit is contained in:
emile 2016-01-29 20:34:17 +01:00
parent bb3b9f61cd
commit c22598c8ff
No known key found for this signature in database
GPG key ID: D808B4C167352E59
9 changed files with 366 additions and 145 deletions

View file

@ -6,6 +6,9 @@ TRAEFIK_ENVS := \
-e TESTFLAGS \ -e TESTFLAGS \
-e CIRCLECI -e CIRCLECI
SRCS = $(shell git ls-files '*.go' | grep -v '^external/')
BIND_DIR := "dist" BIND_DIR := "dist"
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/emilevauge/traefik/$(BIND_DIR)" TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/emilevauge/traefik/$(BIND_DIR)"
@ -78,3 +81,9 @@ generate-webui:
mkdir -p static mkdir -p static
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui gulp 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 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)

View file

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
) )
// OxyLogger implements oxy Logger interface with logrus. // OxyLogger implements oxy Logger interface with logrus.
@ -33,10 +32,3 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) http.NotFound(w, r)
//templatesRenderer.HTML(w, http.StatusNotFound, "notFound", nil) //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
}

12
cmd.go
View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"encoding/json"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/emilevauge/traefik/middlewares" "github.com/emilevauge/traefik/middlewares"
"github.com/emilevauge/traefik/provider" "github.com/emilevauge/traefik/provider"
@ -49,6 +50,7 @@ var arguments = struct {
boltdb bool boltdb bool
}{ }{
GlobalConfiguration{ GlobalConfiguration{
EntryPoints: make(EntryPoints),
Docker: &provider.Docker{ Docker: &provider.Docker{
TLS: &provider.DockerTLS{}, 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().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("accessLogsFile", "log/access.log", "Access logs file")
traefikCmd.PersistentFlags().String("traefikLogsFile", "log/traefik.log", "Traefik 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().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().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") 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("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
viper.BindPFlag("port", traefikCmd.PersistentFlags().Lookup("port")) viper.BindPFlag("port", traefikCmd.PersistentFlags().Lookup("port"))
viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut")) 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")) viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
// TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105 // TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105
viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration")) viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost")) viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost"))
viper.SetDefault("certificates", &Certificates{})
viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second)) viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second))
viper.SetDefault("logLevel", "ERROR")
} }
func run() { func run() {
@ -176,7 +179,8 @@ func run() {
} else { } else {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) 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 := NewServer(*globalConfiguration)
server.Start() server.Start()
defer server.Close() defer server.Close()

View file

@ -11,17 +11,18 @@ import (
"github.com/emilevauge/traefik/types" "github.com/emilevauge/traefik/types"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/spf13/viper" "github.com/spf13/viper"
"regexp"
) )
// GlobalConfiguration holds global configuration (with providers, etc.). // GlobalConfiguration holds global configuration (with providers, etc.).
// It's populated from the traefik configuration file passed as an argument to the binary. // It's populated from the traefik configuration file passed as an argument to the binary.
type GlobalConfiguration struct { type GlobalConfiguration struct {
Port string
GraceTimeOut int64 GraceTimeOut int64
AccessLogsFile string AccessLogsFile string
TraefikLogsFile string TraefikLogsFile string
Certificates Certificates
LogLevel string LogLevel string
EntryPoints EntryPoints
DefaultEntryPoints DefaultEntryPoints
ProvidersThrottleDuration time.Duration ProvidersThrottleDuration time.Duration
MaxIdleConnsPerHost int MaxIdleConnsPerHost int
Docker *provider.Docker Docker *provider.Docker
@ -34,6 +35,110 @@ type GlobalConfiguration struct {
Boltdb *provider.BoltDb 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<Name>\\S*))\\s*(?:Address:(?P<Address>\\S*))?\\s*(?:TLS:(?P<TLS>\\S*))?\\s*(?:Redirect.EntryPoint:(?P<RedirectEntryPoint>\\S*))?\\s*(?:Redirect.Regex:(?P<RedirectRegex>\\S*))?\\s*(?:Redirect.Replacement:(?P<RedirectReplacement>\\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 // Certificates defines traefik certificates type
type Certificates []Certificate type Certificates []Certificate
@ -74,14 +179,7 @@ type Certificate struct {
// NewGlobalConfiguration returns a GlobalConfiguration with default values. // NewGlobalConfiguration returns a GlobalConfiguration with default values.
func NewGlobalConfiguration() *GlobalConfiguration { func NewGlobalConfiguration() *GlobalConfiguration {
globalConfiguration := new(GlobalConfiguration) return new(GlobalConfiguration)
// default values
globalConfiguration.Port = ":80"
globalConfiguration.GraceTimeOut = 10
globalConfiguration.LogLevel = "ERROR"
globalConfiguration.ProvidersThrottleDuration = time.Duration(2 * time.Second)
return globalConfiguration
} }
// LoadConfiguration returns a GlobalConfiguration. // LoadConfiguration returns a GlobalConfiguration.
@ -101,8 +199,12 @@ func LoadConfiguration() *GlobalConfiguration {
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
fmtlog.Fatalf("Error reading file: %s", err) 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 { if arguments.web {
viper.Set("web", arguments.Web) viper.Set("web", arguments.Web)
@ -135,6 +237,19 @@ func LoadConfiguration() *GlobalConfiguration {
fmtlog.Fatalf("Error reading file: %s", err) 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 return configuration
} }

View file

@ -145,7 +145,7 @@ import:
- package: github.com/donovanhide/eventsource - package: github.com/donovanhide/eventsource
ref: d8a3071799b98cacd30b6da92f536050ccfe6da4 ref: d8a3071799b98cacd30b6da92f536050ccfe6da4
- package: github.com/golang/glog - package: github.com/golang/glog
ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f
- package: github.com/spf13/cast - package: github.com/spf13/cast
ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778 ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778
- package: github.com/mitchellh/mapstructure - package: github.com/mitchellh/mapstructure
@ -161,4 +161,5 @@ import:
- package: github.com/spf13/cobra - package: github.com/spf13/cobra
subpackages: subpackages:
- /cobra - /cobra
- package: github.com/vulcand/vulcand/plugin/rewrite

31
middlewares/rewrite.go Normal file
View file

@ -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)
}

307
server.go
View file

@ -5,6 +5,7 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
@ -16,12 +17,12 @@ import (
"github.com/mailgun/oxy/cbreaker" "github.com/mailgun/oxy/cbreaker"
"github.com/mailgun/oxy/forward" "github.com/mailgun/oxy/forward"
"github.com/mailgun/oxy/roundrobin" "github.com/mailgun/oxy/roundrobin"
"github.com/spf13/viper"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
"reflect" "reflect"
"regexp"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -31,11 +32,10 @@ var oxyLogger = &OxyLogger{}
// Server is the reverse-proxy/load-balancer engine // Server is the reverse-proxy/load-balancer engine
type Server struct { type Server struct {
srv *manners.GracefulServer serverEntryPoints map[string]serverEntryPoint
configurationRouter *mux.Router
configurationChan chan types.ConfigMessage configurationChan chan types.ConfigMessage
configurationChanValidated chan types.ConfigMessage configurationValidatedChan chan types.ConfigMessage
sigs chan os.Signal signals chan os.Signal
stopChan chan bool stopChan chan bool
providers []provider.Provider providers []provider.Provider
serverLock sync.Mutex serverLock sync.Mutex
@ -44,16 +44,22 @@ type Server struct {
loggerMiddleware *middlewares.Logger loggerMiddleware *middlewares.Logger
} }
type serverEntryPoint struct {
httpServer *manners.GracefulServer
httpRouter *mux.Router
}
// NewServer returns an initialized Server. // NewServer returns an initialized Server.
func NewServer(globalConfiguration GlobalConfiguration) *Server { func NewServer(globalConfiguration GlobalConfiguration) *Server {
server := new(Server) server := new(Server)
server.serverEntryPoints = make(map[string]serverEntryPoint)
server.configurationChan = make(chan types.ConfigMessage, 10) server.configurationChan = make(chan types.ConfigMessage, 10)
server.configurationChanValidated = make(chan types.ConfigMessage, 10) server.configurationValidatedChan = make(chan types.ConfigMessage, 10)
server.sigs = make(chan os.Signal, 1) server.signals = make(chan os.Signal, 1)
server.stopChan = make(chan bool) server.stopChan = make(chan bool)
server.providers = []provider.Provider{} 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.currentConfigurations = make(configs)
server.globalConfiguration = globalConfiguration server.globalConfiguration = globalConfiguration
server.loggerMiddleware = middlewares.NewLogger(globalConfiguration.AccessLogsFile) 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. // Start starts the server and blocks until server is shutted down.
func (server *Server) Start() { func (server *Server) Start() {
server.configurationRouter = LoadDefaultConfig(server.globalConfiguration)
go server.listenProviders() go server.listenProviders()
go server.enableRouter() go server.listenConfigurations()
server.configureProviders() server.configureProviders()
server.startProviders() server.startProviders()
go server.listenSignals() 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 <-server.stopChan
} }
// Stop stops the server // Stop stops the server
func (server *Server) Stop() { func (server *Server) Stop() {
server.srv.Close() for _, serverEntryPoint := range server.serverEntryPoints {
serverEntryPoint.httpServer.Close()
}
server.stopChan <- true server.stopChan <- true
} }
// Close destroys the server // Close destroys the server
func (server *Server) Close() { func (server *Server) Close() {
close(server.configurationChan) close(server.configurationChan)
close(server.configurationChanValidated) close(server.configurationValidatedChan)
close(server.sigs) close(server.signals)
close(server.stopChan) close(server.stopChan)
server.loggerMiddleware.Close() server.loggerMiddleware.Close()
} }
@ -104,19 +99,20 @@ func (server *Server) listenProviders() {
lastConfigs := make(map[string]*types.ConfigMessage) lastConfigs := make(map[string]*types.ConfigMessage)
for { for {
configMsg := <-server.configurationChan 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 lastConfigs[configMsg.ProviderName] = &configMsg
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) { 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) log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
// last config received more than n s ago // last config received more than n s ago
server.configurationChanValidated <- configMsg server.configurationValidatedChan <- configMsg
} else { } else {
log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration) log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
go func() { go func() {
<-time.After(server.globalConfiguration.ProvidersThrottleDuration) <-time.After(server.globalConfiguration.ProvidersThrottleDuration)
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) { if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
log.Debugf("Waited for %s config, OK", configMsg.ProviderName) 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 { for {
configMsg := <-server.configurationChanValidated configMsg := <-server.configurationValidatedChan
if configMsg.Configuration == nil { if configMsg.Configuration == nil {
log.Info("Skipping empty Configuration") log.Info("Skipping empty Configuration")
} else if reflect.DeepEqual(server.currentConfigurations[configMsg.ProviderName], configMsg.Configuration) { } else if reflect.DeepEqual(server.currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
@ -139,22 +135,26 @@ func (server *Server) enableRouter() {
} }
newConfigurations[configMsg.ProviderName] = configMsg.Configuration newConfigurations[configMsg.ProviderName] = configMsg.Configuration
newConfigurationRouter, err := server.loadConfig(newConfigurations, server.globalConfiguration) newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration)
if err == nil { if err == nil {
server.serverLock.Lock() server.serverLock.Lock()
server.currentConfigurations = newConfigurations for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints {
server.configurationRouter = newConfigurationRouter currentServerEntryPoint := server.serverEntryPoints[newServerEntryPointName]
oldServer := server.srv server.currentConfigurations = newConfigurations
newsrv, err := server.prepareServer(server.configurationRouter, server.globalConfiguration, oldServer, server.loggerMiddleware, metrics) currentServerEntryPoint.httpRouter = newServerEntryPoint.httpRouter
if err != nil { oldServer := currentServerEntryPoint.httpServer
log.Fatal("Error preparing server: ", err) newsrv, err := server.prepareServer(currentServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], oldServer, server.loggerMiddleware, metrics)
} if err != nil {
go server.startServer(newsrv, server.globalConfiguration) log.Fatal("Error preparing server: ", err)
server.srv = newsrv }
time.Sleep(1 * time.Second) go server.startServer(newsrv, server.globalConfiguration)
if oldServer != nil { currentServerEntryPoint.httpServer = newsrv
log.Info("Stopping old server") server.serverEntryPoints[newServerEntryPointName] = currentServerEntryPoint
oldServer.Close() time.Sleep(1 * time.Second)
if oldServer != nil {
log.Info("Stopping old server")
oldServer.Close()
}
} }
server.serverLock.Unlock() server.serverLock.Unlock()
} else { } else {
@ -173,10 +173,6 @@ func (server *Server) configureProviders() {
server.providers = append(server.providers, server.globalConfiguration.Marathon) server.providers = append(server.providers, server.globalConfiguration.Marathon)
} }
if server.globalConfiguration.File != nil { 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) server.providers = append(server.providers, server.globalConfiguration.File)
} }
if server.globalConfiguration.Web != nil { if server.globalConfiguration.Web != nil {
@ -200,7 +196,8 @@ func (server *Server) configureProviders() {
func (server *Server) startProviders() { func (server *Server) startProviders() {
// start providers // start providers
for _, provider := range server.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 currentProvider := provider
go func() { go func() {
err := currentProvider.Provide(server.configurationChan) err := currentProvider.Provide(server.configurationChan)
@ -212,15 +209,18 @@ func (server *Server) startProviders() {
} }
func (server *Server) listenSignals() { func (server *Server) listenSignals() {
sig := <-server.sigs sig := <-server.signals
log.Infof("I have to go... %+v", sig) log.Infof("I have to go... %+v", sig)
log.Info("Stopping server") log.Info("Stopping server")
server.Stop() server.Stop()
} }
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI // creates a TLS config that allows terminating HTTPS for multiple domains using SNI
func (server *Server) createTLSConfig(certs []Certificate) (*tls.Config, error) { func (server *Server) createTLSConfig(tlsOption *TLS) (*tls.Config, error) {
if len(certs) == 0 { if tlsOption == nil {
return nil, nil
}
if len(tlsOption.Certificates) == 0 {
return nil, nil return nil, nil
} }
@ -230,8 +230,8 @@ func (server *Server) createTLSConfig(certs []Certificate) (*tls.Config, error)
} }
var err error var err error
config.Certificates = make([]tls.Certificate, len(certs)) config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates))
for i, v := range certs { for i, v := range tlsOption.Certificates {
config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile) config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
if err != nil { if err != nil {
return nil, err 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) { func (server *Server) startServer(srv *manners.GracefulServer, globalConfiguration GlobalConfiguration) {
log.Info("Starting server") log.Info("Starting server on ", srv.Addr)
if srv.TLSConfig != nil { if srv.TLSConfig != nil {
err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig) err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig)
if err != nil { if err != nil {
@ -259,7 +259,7 @@ func (server *Server) startServer(srv *manners.GracefulServer, globalConfigurati
log.Info("Server stopped") 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") log.Info("Preparing server")
// middlewares // middlewares
var negroni = negroni.New() var negroni = negroni.New()
@ -267,7 +267,7 @@ func (server *Server) prepareServer(router *mux.Router, globalConfiguration Glob
negroni.Use(middleware) negroni.Use(middleware)
} }
negroni.UseHandler(router) negroni.UseHandler(router)
tlsConfig, err := server.createTLSConfig(globalConfiguration.Certificates) tlsConfig, err := server.createTLSConfig(entryPoint.TLS)
if err != nil { if err != nil {
log.Fatalf("Error creating TLS config %s", err) log.Fatalf("Error creating TLS config %s", err)
return nil, err return nil, err
@ -276,13 +276,13 @@ func (server *Server) prepareServer(router *mux.Router, globalConfiguration Glob
if oldServer == nil { if oldServer == nil {
return manners.NewWithServer( return manners.NewWithServer(
&http.Server{ &http.Server{
Addr: globalConfiguration.Port, Addr: entryPoint.Address,
Handler: negroni, Handler: negroni,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
}), nil }), nil
} }
gracefulServer, err := oldServer.HijackListener(&http.Server{ gracefulServer, err := oldServer.HijackListener(&http.Server{
Addr: globalConfiguration.Port, Addr: entryPoint.Address,
Handler: negroni, Handler: negroni,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
}, tlsConfig) }, tlsConfig)
@ -293,80 +293,147 @@ func (server *Server) prepareServer(router *mux.Router, globalConfiguration Glob
return gracefulServer, nil 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 // LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic
// provider configurations. // provider configurations.
func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (*mux.Router, error) { func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (map[string]serverEntryPoint, error) {
router := mux.NewRouter() serverEntryPoints := server.buildEntryPoints(globalConfiguration)
router.NotFoundHandler = http.HandlerFunc(notFoundHandler) redirectHandlers := make(map[string]http.Handler)
backends := map[string]http.Handler{} backends := map[string]http.Handler{}
for _, configuration := range configurations { for _, configuration := range configurations {
for frontendName, frontend := range configuration.Frontends { 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)) fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader))
newRoute := router.NewRoute().Name(frontendName) // default endpoints if not defined in frontends
for routeName, route := range frontend.Routes { if len(frontend.EntryPoints) == 0 {
log.Infof("Creating route %s %s:%s", routeName, route.Rule, route.Value) frontend.EntryPoints = globalConfiguration.DefaultEntryPoints
newRouteReflect, err := invoke(newRoute, route.Rule, route.Value)
if err != nil {
return nil, err
}
newRoute = newRouteReflect[0].Interface().(*mux.Route)
} }
if backends[frontend.Backend] == nil { for _, entryPointName := range frontend.EntryPoints {
log.Infof("Creating backend %s", frontend.Backend) log.Debugf("Wiring frontend %s to entryPoint %s", frontendName, entryPointName)
var lb http.Handler if _, ok := serverEntryPoints[entryPointName]; !ok {
rr, _ := roundrobin.New(fwd) return nil, errors.New("Undefined entrypoint: " + entryPointName)
if configuration.Backends[frontend.Backend] == nil {
return nil, errors.New("Backend not found: " + frontend.Backend)
} }
lbMethod, err := types.NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer) newRoute := serverEntryPoints[entryPointName].httpRouter.NewRoute().Name(frontendName)
if err != nil { for routeName, route := range frontend.Routes {
configuration.Backends[frontend.Backend].LoadBalancer = &types.LoadBalancer{Method: "wrr"} log.Debugf("Creating route %s %s:%s", routeName, route.Rule, route.Value)
} newRouteReflect, err := invoke(newRoute, route.Rule, route.Value)
switch lbMethod { if err != nil {
case types.Drr: return nil, err
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 = newRouteReflect[0].Interface().(*mux.Route)
} }
var negroni = negroni.New() entryPoint := globalConfiguration.EntryPoints[entryPointName]
if configuration.Backends[frontend.Backend].CircuitBreaker != nil { if entryPoint.Redirect != nil {
log.Infof("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression) if redirectHandlers[entryPointName] != nil {
negroni.Use(middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger))) 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 { } 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
} }

View file

@ -36,6 +36,7 @@ type Route struct {
// Frontend holds frontend configuration. // Frontend holds frontend configuration.
type Frontend struct { type Frontend struct {
EntryPoints []string `json:"entryPoints,omitempty"`
Backend string `json:"backend,omitempty"` Backend string `json:"backend,omitempty"`
Routes map[string]Route `json:"routes,omitempty"` Routes map[string]Route `json:"routes,omitempty"`
PassHostHeader bool `json:"passHostHeader,omitempty"` PassHostHeader bool `json:"passHostHeader,omitempty"`

View file

@ -17,6 +17,7 @@
</table> </table>
</div> </div>
<div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer"> <div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer">
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints"><span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last">&nbsp;</span></span>
<span class="label label-warning" role="button" data-toggle="collapse" href="#{{frontendCtrl.frontend.backend}}" aria-expanded="false">{{frontendCtrl.frontend.backend}}</span> <span class="label label-warning" role="button" data-toggle="collapse" href="#{{frontendCtrl.frontend.backend}}" aria-expanded="false">{{frontendCtrl.frontend.backend}}</span>
<span data-ng-show="frontendCtrl.frontend.passHostHeader" class="label label-warning">Pass Host Header</span> <span data-ng-show="frontendCtrl.frontend.passHostHeader" class="label label-warning">Pass Host Header</span>
</div> </div>