This commit is contained in:
emile 2015-09-11 16:37:13 +02:00
parent ee13d570d8
commit 85e1af878a
8 changed files with 288 additions and 47 deletions

View file

@ -1,15 +1,24 @@
# /træfɪk/
* Logs
* Default configuration values * Default configuration values
* Godoc * Retry with streams
* Static files
* Licence * Licence
* Add traefik.indlude all/enabled policy * Add traefik.indlude all/enabled policy
* SSL frontend support * SSL frontend support
* SSL backends support * SSL backends support
* Etcd support
* Consul support * Consul support
* Kubernetes support
* Smart configuration diff
* README * README
* API enhancements * API enhancements
* Godoc
* Website
* Kubernetes support
* Etcd support
* Smart configuration diff
* ~~GraceTimeout~~ * ~~GraceTimeout~~
* ~~Weights~~ * ~~Weights~~

View file

@ -1,12 +1,16 @@
package main package main
type GlobalConfiguration struct { type GlobalConfiguration struct {
Port string Port string
GraceTimeOut int64 GraceTimeOut int64
Docker *DockerProvider AccessLogsFile string
File *FileProvider TraefikLogsFile string
Web *WebProvider TraefikLogsStdout bool
Marathon *MarathonProvider LogLevel string
Docker *DockerProvider
File *FileProvider
Web *WebProvider
Marathon *MarathonProvider
} }
func NewGlobalConfiguration() *GlobalConfiguration { func NewGlobalConfiguration() *GlobalConfiguration {
@ -23,7 +27,7 @@ type Backend struct {
} }
type Server struct { type Server struct {
Url string Url string
Weight int Weight int
} }
@ -34,7 +38,7 @@ type Rule struct {
type Route struct { type Route struct {
Backend string Backend string
Rules map[string]Rule Rules map[string]Rule
} }
type Configuration struct { type Configuration struct {

View file

@ -4,7 +4,6 @@ import (
"github.com/leekchan/gtf" "github.com/leekchan/gtf"
"bytes" "bytes"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"log"
"text/template" "text/template"
"strings" "strings"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
@ -65,7 +64,7 @@ func (provider *DockerProvider) Provide(configurationChan chan <- *Configuration
for { for {
event := <-dockerEvents event := <-dockerEvents
if(event.Status == "start" || event.Status == "die"){ if(event.Status == "start" || event.Status == "die"){
log.Println("Docker event receveived", event) log.Debug("Docker event receveived %+v", event)
configuration := provider.loadDockerConfig() configuration := provider.loadDockerConfig()
if (configuration != nil) { if (configuration != nil) {
configurationChan <- configuration configurationChan <- configuration
@ -95,16 +94,16 @@ func (provider *DockerProvider) loadDockerConfig() *Configuration {
// filter containers // filter containers
filteredContainers := fun.Filter(func(container docker.Container) bool { filteredContainers := fun.Filter(func(container docker.Container) bool {
if (len(container.NetworkSettings.Ports) == 0) { if (len(container.NetworkSettings.Ports) == 0) {
log.Println("Filtering container without port", container.Name) log.Debug("Filtering container without port %s", container.Name)
return false return false
} }
_, err := strconv.Atoi(container.Config.Labels["traefik.port"]) _, err := strconv.Atoi(container.Config.Labels["traefik.port"])
if (len(container.NetworkSettings.Ports) > 1 && err != nil) { if (len(container.NetworkSettings.Ports) > 1 && err != nil) {
log.Println("Filtering container with more than 1 port and no traefik.port label", container.Name) log.Debug("Filtering container with more than 1 port and no traefik.port label %s", container.Name)
return false return false
} }
if (container.Config.Labels["traefik.enable"] == "false") { if (container.Config.Labels["traefik.enable"] == "false") {
log.Println("Filtering disabled container", container.Name) log.Debug("Filtering disabled container %s", container.Name)
return false return false
} }
return true return true
@ -126,7 +125,7 @@ func (provider *DockerProvider) loadDockerConfig() *Configuration {
gtf.Inject(DockerFuncMap) gtf.Inject(DockerFuncMap)
tmpl, err := template.New(provider.Filename).Funcs(DockerFuncMap).ParseFiles(provider.Filename) tmpl, err := template.New(provider.Filename).Funcs(DockerFuncMap).ParseFiles(provider.Filename)
if err != nil { if err != nil {
log.Println("Error reading file:", err) log.Error("Error reading file", err)
return nil return nil
} }
@ -134,12 +133,12 @@ func (provider *DockerProvider) loadDockerConfig() *Configuration {
err = tmpl.Execute(&buffer, templateObjects) err = tmpl.Execute(&buffer, templateObjects)
if err != nil { if err != nil {
log.Println("Error with docker template:", err) log.Error("Error with docker template", err)
return nil return nil
} }
if _, err := toml.Decode(buffer.String(), configuration); err != nil { if _, err := toml.Decode(buffer.String(), configuration); err != nil {
log.Println("Error creating docker configuration:", err) log.Error("Error creating docker configuration", err)
return nil return nil
} }
return configuration return configuration

13
file.go
View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"log"
"gopkg.in/fsnotify.v1" "gopkg.in/fsnotify.v1"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"os" "os"
@ -17,14 +16,14 @@ type FileProvider struct {
func (provider *FileProvider) Provide(configurationChan chan<- *Configuration){ func (provider *FileProvider) Provide(configurationChan chan<- *Configuration){
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
log.Println(err) log.Error("Error creating file watcher", err)
return return
} }
defer watcher.Close() defer watcher.Close()
file, err := os.Open(provider.Filename) file, err := os.Open(provider.Filename)
if err != nil { if err != nil {
log.Println(err) log.Error("Error opening file", err)
return return
} }
defer file.Close() defer file.Close()
@ -36,14 +35,14 @@ func (provider *FileProvider) Provide(configurationChan chan<- *Configuration){
select { select {
case event := <-watcher.Events: case event := <-watcher.Events:
if(strings.Contains(event.Name,file.Name())){ if(strings.Contains(event.Name,file.Name())){
log.Println("File event:", event) log.Debug("File event:", event)
configuration := provider.LoadFileConfig(file.Name()) configuration := provider.LoadFileConfig(file.Name())
if(configuration != nil) { if(configuration != nil) {
configurationChan <- configuration configurationChan <- configuration
} }
} }
case error := <-watcher.Errors: case error := <-watcher.Errors:
log.Println("error:", error) log.Error("Watcher event error", error)
} }
} }
}() }()
@ -53,7 +52,7 @@ func (provider *FileProvider) Provide(configurationChan chan<- *Configuration){
} }
if err != nil { if err != nil {
log.Println(err) log.Error("Error adding file watcher", err)
return return
} }
@ -67,7 +66,7 @@ func (provider *FileProvider) Provide(configurationChan chan<- *Configuration){
func (provider *FileProvider) LoadFileConfig(filename string) *Configuration { func (provider *FileProvider) LoadFileConfig(filename string) *Configuration {
configuration := new(Configuration) configuration := new(Configuration)
if _, err := toml.DecodeFile(filename, configuration); err != nil { if _, err := toml.DecodeFile(filename, configuration); err != nil {
log.Println("Error reading file:", err) log.Error("Error reading file:", err)
return nil return nil
} }
return configuration return configuration

View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
"log"
"github.com/leekchan/gtf" "github.com/leekchan/gtf"
"bytes" "bytes"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
@ -47,24 +46,25 @@ var MarathonFuncMap = template.FuncMap{
return strings.Replace(s3, s1, s2, -1) return strings.Replace(s3, s1, s2, -1)
}, },
} }
func (provider *MarathonProvider) Provide(configurationChan chan <- *Configuration) { func (provider *MarathonProvider) Provide(configurationChan chan <- *Configuration) {
config := marathon.NewDefaultConfig() config := marathon.NewDefaultConfig()
config.URL = provider.Endpoint config.URL = provider.Endpoint
config.EventsInterface = provider.NetworkInterface config.EventsInterface = provider.NetworkInterface
if client, err := marathon.NewClient(config); err != nil { if client, err := marathon.NewClient(config); err != nil {
log.Println("Failed to create a client for marathon, error: %s", err) log.Error("Failed to create a client for marathon, error: %s", err)
return return
} else { } else {
provider.marathonClient = client provider.marathonClient = client
update := make(marathon.EventsChannel, 5) update := make(marathon.EventsChannel, 5)
if (provider.Watch) { if (provider.Watch) {
if err := client.AddEventsListener(update, marathon.EVENTS_APPLICATIONS); err != nil { if err := client.AddEventsListener(update, marathon.EVENTS_APPLICATIONS); err != nil {
log.Println("Failed to register for subscriptions, %s", err) log.Error("Failed to register for subscriptions, %s", err)
} else { } else {
go func() { go func() {
for { for {
event := <-update event := <-update
log.Println("Marathon event receveived", event) log.Debug("Marathon event receveived", event)
configuration := provider.loadMarathonConfig() configuration := provider.loadMarathonConfig()
if (configuration != nil) { if (configuration != nil) {
configurationChan <- configuration configurationChan <- configuration
@ -84,30 +84,30 @@ func (provider *MarathonProvider) loadMarathonConfig() *Configuration {
applications, err := provider.marathonClient.Applications(nil) applications, err := provider.marathonClient.Applications(nil)
if (err != nil) { if (err != nil) {
log.Println("Failed to create a client for marathon, error: %s", err) log.Error("Failed to create a client for marathon, error: %s", err)
return nil return nil
} }
tasks, err := provider.marathonClient.AllTasks() tasks, err := provider.marathonClient.AllTasks()
if (err != nil) { if (err != nil) {
log.Println("Failed to create a client for marathon, error: %s", err) log.Error("Failed to create a client for marathon, error: %s", err)
return nil return nil
} }
//filter tasks //filter tasks
filteredTasks := fun.Filter(func(task marathon.Task) bool { filteredTasks := fun.Filter(func(task marathon.Task) bool {
if (len(task.Ports) == 0) { if (len(task.Ports) == 0) {
log.Println("Filtering marathon task without port", task.AppID) log.Debug("Filtering marathon task without port", task.AppID)
return false return false
} }
application := getApplication(task, applications.Apps) application := getApplication(task, applications.Apps)
_, err := strconv.Atoi(application.Labels["traefik.port"]) _, err := strconv.Atoi(application.Labels["traefik.port"])
if (len(application.Ports) > 1 && err != nil) { if (len(application.Ports) > 1 && err != nil) {
log.Println("Filtering marathon task with more than 1 port and no traefik.port label", task.AppID) log.Debug("Filtering marathon task with more than 1 port and no traefik.port label", task.AppID)
return false return false
} }
if (application.Labels["traefik.enable"] == "false") { if (application.Labels["traefik.enable"] == "false") {
log.Println("Filtering disabled marathon task", task.AppID) log.Debug("Filtering disabled marathon task", task.AppID)
return false return false
} }
return true return true
@ -140,7 +140,7 @@ func (provider *MarathonProvider) loadMarathonConfig() *Configuration {
gtf.Inject(MarathonFuncMap) gtf.Inject(MarathonFuncMap)
tmpl, err := template.New(provider.Filename).Funcs(MarathonFuncMap).ParseFiles(provider.Filename) tmpl, err := template.New(provider.Filename).Funcs(MarathonFuncMap).ParseFiles(provider.Filename)
if err != nil { if err != nil {
log.Println("Error reading file:", err) log.Error("Error reading file:", err)
return nil return nil
} }
@ -148,12 +148,12 @@ func (provider *MarathonProvider) loadMarathonConfig() *Configuration {
err = tmpl.Execute(&buffer, templateObjects) err = tmpl.Execute(&buffer, templateObjects)
if err != nil { if err != nil {
log.Println("Error with docker template:", err) log.Error("Error with docker template:", err)
return nil return nil
} }
if _, err := toml.Decode(buffer.String(), configuration); err != nil { if _, err := toml.Decode(buffer.String(), configuration); err != nil {
log.Println("Error creating marathon configuration:", err) log.Error("Error creating marathon configuration:", err)
return nil return nil
} }

229
traefik.go Normal file
View file

@ -0,0 +1,229 @@
package main
import (
"github.com/gorilla/mux"
"github.com/mailgun/oxy/forward"
"github.com/mailgun/oxy/roundrobin"
"github.com/tylerb/graceful"
"net/http"
"net/url"
"os"
"os/signal"
"reflect"
"syscall"
"time"
"github.com/op/go-logging"
"github.com/BurntSushi/toml"
"github.com/gorilla/handlers"
)
var currentConfiguration = new(Configuration)
var log = logging.MustGetLogger("traefik")
func main() {
var srv *graceful.Server
var configurationRouter *mux.Router
var configurationChan = make(chan *Configuration)
var providers = []Provider{}
var format = logging.MustStringFormatter("%{color}%{time:15:04:05.000} %{shortfile:20.20s} %{level:8.8s} %{id:03x} ▶%{color:reset} %{message}")
var sigs = make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
// load global configuration
globalConfigFile := "traefik.toml"
gloablConfiguration := LoadFileConfig(globalConfigFile)
// logging
backends := []logging.Backend{}
level, err := logging.LogLevel(gloablConfiguration.LogLevel)
if err != nil {
log.Fatal("Error getting level", err)
}
if (len(gloablConfiguration.TraefikLogsFile) > 0 ){
fi, err := os.OpenFile(gloablConfiguration.TraefikLogsFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatal("Error opening file", err)
}else{
logBackend := logging.NewLogBackend(fi, "", 0)
logBackendFormatter := logging.NewBackendFormatter(logBackend, logging.GlogFormatter)
logBackendLeveled := logging.AddModuleLevel(logBackend)
logBackendLeveled.SetLevel(level, "")
backends = append(backends, logBackendFormatter)
}
}
if (gloablConfiguration.TraefikLogsStdout){
logBackend := logging.NewLogBackend(os.Stdout, "", 0)
logBackendFormatter := logging.NewBackendFormatter(logBackend, format)
logBackendLeveled := logging.AddModuleLevel(logBackend)
logBackendLeveled.SetLevel(level, "")
backends = append(backends, logBackendFormatter)
}
logging.SetBackend(backends...)
configurationRouter = LoadDefaultConfig(gloablConfiguration)
// listen new configurations from providers
go func() {
for {
configuration := <-configurationChan
log.Info("Configuration receveived %+v", configuration)
if configuration == nil {
log.Info("Skipping empty configuration")
} else if (reflect.DeepEqual(currentConfiguration, configuration)) {
log.Info("Skipping same configuration")
} else {
currentConfiguration = configuration
configurationRouter = LoadConfig(configuration, gloablConfiguration)
srv.Stop(10 * time.Second)
time.Sleep(3 * time.Second)
}
}
}()
// configure providers
if (gloablConfiguration.Docker != nil) {
providers = append(providers, gloablConfiguration.Docker)
}
if (gloablConfiguration.Marathon != nil) {
providers = append(providers, gloablConfiguration.Marathon)
}
if (gloablConfiguration.File != nil) {
if (len(gloablConfiguration.File.Filename) == 0) {
// no filename, setting to global config file
gloablConfiguration.File.Filename = globalConfigFile
}
providers = append(providers, gloablConfiguration.File)
}
if (gloablConfiguration.Web != nil) {
providers = append(providers, gloablConfiguration.Web)
}
// start providers
for _, provider := range providers {
log.Notice("Starting provider %v %+v", reflect.TypeOf(provider), provider)
currentProvider := provider
go func() {
currentProvider.Provide(configurationChan)
}()
}
goAway := false
go func() {
sig := <-sigs
log.Notice("I have to go... %+v", sig)
goAway = true
srv.Stop(time.Duration(gloablConfiguration.GraceTimeOut) * time.Second)
}()
for {
if goAway {
break
}
srv = &graceful.Server{
Timeout: time.Duration(gloablConfiguration.GraceTimeOut) * time.Second,
NoSignalHandling: true,
Server: &http.Server{
Addr: gloablConfiguration.Port,
Handler: configurationRouter,
},
}
go func() {
srv.ListenAndServe()
}()
log.Notice("Started")
<-srv.StopChan()
log.Notice("Stopped")
}
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
renderer.HTML(w, http.StatusNotFound, "notFound", nil)
}
func LoadDefaultConfig(gloablConfiguration *GlobalConfiguration) *mux.Router {
router := mux.NewRouter()
if (len(gloablConfiguration.AccessLogsFile) > 0 ){
fi, err := os.OpenFile(gloablConfiguration.AccessLogsFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatal("Error opening file", err)
}
router.NotFoundHandler = handlers.CombinedLoggingHandler(fi, http.HandlerFunc(notFoundHandler))
}else{
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
}
return router
}
func LoadConfig(configuration *Configuration, gloablConfiguration *GlobalConfiguration) *mux.Router {
router := mux.NewRouter()
if (len(gloablConfiguration.AccessLogsFile) > 0 ){
fi, err := os.OpenFile(gloablConfiguration.AccessLogsFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatal("Error opening file", err)
}
router.NotFoundHandler = handlers.CombinedLoggingHandler(fi, http.HandlerFunc(notFoundHandler))
}else{
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
}
backends := map[string]http.Handler{}
for routeName, route := range configuration.Routes {
log.Debug("Creating route %s", routeName)
fwd, _ := forward.New()
newRoutes := []*mux.Route{}
for ruleName, rule := range route.Rules {
log.Debug("Creating rule %s", ruleName)
newRouteReflect := Invoke(router.NewRoute(), rule.Category, rule.Value)
newRoute := newRouteReflect[0].Interface().(*mux.Route)
newRoutes = append(newRoutes, newRoute)
}
if (backends[route.Backend] ==nil) {
log.Debug("Creating backend %s", route.Backend)
lb, _ := roundrobin.New(fwd)
rb, _ := roundrobin.NewRebalancer(lb)
for serverName, server := range configuration.Backends[route.Backend].Servers {
log.Debug("Creating server %s", serverName)
url, _ := url.Parse(server.Url)
rb.UpsertServer(url, roundrobin.Weight(server.Weight))
}
backends[route.Backend]=lb
}else {
log.Debug("Reusing backend", route.Backend)
}
for _, muxRoute := range newRoutes {
if (len(gloablConfiguration.AccessLogsFile) > 0 ){
fi, err := os.OpenFile(gloablConfiguration.AccessLogsFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatal("Error opening file", err)
}
muxRoute.Handler(handlers.CombinedLoggingHandler(fi, backends[route.Backend]))
}else{
muxRoute.Handler(backends[route.Backend])
}
err := muxRoute.GetError()
if err != nil {
log.Error("Error building route", err)
}
}
}
return router
}
func Invoke(any interface{}, name string, args ...interface{}) []reflect.Value {
inputs := make([]reflect.Value, len(args))
for i, _ := range args {
inputs[i] = reflect.ValueOf(args[i])
}
return reflect.ValueOf(any).MethodByName(name).Call(inputs)
}
func LoadFileConfig(file string) *GlobalConfiguration {
configuration := NewGlobalConfiguration()
if _, err := toml.DecodeFile(file, configuration); err != nil {
log.Fatal("Error reading file", err)
}
log.Debug("Global configuration loaded %+v", configuration)
return configuration
}

View file

@ -1,5 +1,9 @@
port = ":8001" port = ":8001"
graceTimeOut = 10 graceTimeOut = 10
traefikLogsFile = "log/traefik.log"
traefikLogsStdout = true
accessLogsFile = "log/access.log"
logLevel = "DEBUG"
[docker] [docker]
endpoint = "unix:///var/run/docker.sock" endpoint = "unix:///var/run/docker.sock"

13
web.go
View file

@ -3,13 +3,10 @@ package main
import ( import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"net/http" "net/http"
"os"
"github.com/gorilla/handlers"
"github.com/unrolled/render" "github.com/unrolled/render"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"encoding/json" "encoding/json"
"log"
) )
var renderer = render.New() var renderer = render.New()
@ -24,9 +21,9 @@ type Page struct {
func (provider *WebProvider) Provide(configurationChan chan<- *Configuration){ func (provider *WebProvider) Provide(configurationChan chan<- *Configuration){
systemRouter := mux.NewRouter() systemRouter := mux.NewRouter()
systemRouter.Methods("GET").PathPrefix("/web/").Handler(handlers.CombinedLoggingHandler(os.Stdout, http.HandlerFunc(GetHtmlConfigHandler))) systemRouter.Methods("GET").PathPrefix("/web/").Handler(http.HandlerFunc(GetHtmlConfigHandler))
systemRouter.Methods("GET").PathPrefix("/api/").Handler(handlers.CombinedLoggingHandler(os.Stdout, http.HandlerFunc(GetConfigHandler))) systemRouter.Methods("GET").PathPrefix("/api/").Handler(http.HandlerFunc(GetConfigHandler))
systemRouter.Methods("POST").PathPrefix("/api/").Handler(handlers.CombinedLoggingHandler(os.Stdout, http.HandlerFunc( systemRouter.Methods("POST").PathPrefix("/api/").Handler(http.HandlerFunc(
func(rw http.ResponseWriter, r *http.Request){ func(rw http.ResponseWriter, r *http.Request){
configuration := new(Configuration) configuration := new(Configuration)
b, _ := ioutil.ReadAll(r.Body) b, _ := ioutil.ReadAll(r.Body)
@ -35,10 +32,10 @@ func (provider *WebProvider) Provide(configurationChan chan<- *Configuration){
configurationChan <- configuration configurationChan <- configuration
GetConfigHandler(rw, r) GetConfigHandler(rw, r)
}else{ }else{
log.Printf("Error parsing configuration %+v\n", err) log.Error("Error parsing configuration %+v\n", err)
http.Error(rw, fmt.Sprintf("%+v", err), http.StatusBadRequest) http.Error(rw, fmt.Sprintf("%+v", err), http.StatusBadRequest)
} }
}))) }))
systemRouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) systemRouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
go http.ListenAndServe(provider.Address, systemRouter) go http.ListenAndServe(provider.Address, systemRouter)