diff --git a/cmd.go b/cmd.go new file mode 100644 index 000000000..81c804d27 --- /dev/null +++ b/cmd.go @@ -0,0 +1,180 @@ +/* +Copyright +*/ +package main + +import ( + fmtlog "log" + "os" + "strings" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/emilevauge/traefik/middlewares" + "github.com/emilevauge/traefik/provider" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var traefikCmd = &cobra.Command{ + Use: "traefik", + Short: "traefik, a modern reverse proxy", + Long: `traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. +Complete documentation is available at http://traefik.io`, + Run: func(cmd *cobra.Command, args []string) { + run() + }, +} +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version", + Long: `Print version`, + Run: func(cmd *cobra.Command, args []string) { + fmtlog.Println(Version + " built on the " + BuildDate) + os.Exit(0) + }, +} + +var arguments = struct { + GlobalConfiguration + web bool + file bool + docker bool + dockerTLS bool + marathon bool + consul bool + zookeeper bool + etcd bool + boltdb bool +}{ + GlobalConfiguration{ + Docker: &provider.Docker{ + TLS: &provider.DockerTLS{}, + }, + File: &provider.File{}, + Web: &WebProvider{}, + Marathon: &provider.Marathon{}, + Consul: &provider.Consul{}, + Zookeeper: &provider.Zookepper{}, + Etcd: &provider.Etcd{}, + Boltdb: &provider.BoltDb{}, + }, + false, + false, + false, + false, + false, + false, + false, + false, + false, +} + +func init() { + traefikCmd.AddCommand(versionCmd) + traefikCmd.PersistentFlags().StringP("configFile", "c", "", "Configuration file to use (TOML, JSON, YAML, HCL).") + traefikCmd.PersistentFlags().StringP("port", "p", ":80", "Reverse proxy port") + 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. You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI") + 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().BoolVar(&arguments.web, "web", false, "Enable Web backend") + traefikCmd.PersistentFlags().StringVar(&arguments.Web.Address, "web.address", ":8080", "Web administration port") + traefikCmd.PersistentFlags().StringVar(&arguments.Web.CertFile, "web.cerFile", "", "SSL certificate") + traefikCmd.PersistentFlags().StringVar(&arguments.Web.KeyFile, "web.keyFile", "", "SSL certificate") + traefikCmd.PersistentFlags().BoolVar(&arguments.Web.ReadOnly, "web.readOnly", false, "Enable read only API") + + traefikCmd.PersistentFlags().BoolVar(&arguments.file, "file", false, "Enable File backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.File.Watch, "file.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.File.Filename, "file.filename", "", "Override default configuration template. For advanced users :)") + + traefikCmd.PersistentFlags().BoolVar(&arguments.docker, "docker", false, "Enable Docker backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.Docker.Watch, "docker.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Filename, "docker.filename", "", "Override default configuration template. For advanced users :)") + traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Endpoint, "docker.endpoint", "unix:///var/run/docker.sock", "Docker server endpoint. Can be a tcp or a unix socket endpoint") + traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Domain, "docker.domain", "", "Default domain used") + traefikCmd.PersistentFlags().BoolVar(&arguments.dockerTLS, "docker.tls", false, "Enable Docker TLS support") + traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.CA, "docker.tls.ca", "", "TLS CA") + traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.Cert, "docker.tls.cert", "", "TLS cert") + traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.Key, "docker.tls.key", "", "TLS key") + traefikCmd.PersistentFlags().BoolVar(&arguments.Docker.TLS.InsecureSkipVerify, "docker.tls.insecureSkipVerify", false, "TLS insecure skip verify") + + traefikCmd.PersistentFlags().BoolVar(&arguments.marathon, "marathon", false, "Enable Marathon backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.Marathon.Watch, "marathon.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Filename, "marathon.filename", "", "Override default configuration template. For advanced users :)") + traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Endpoint, "marathon.endpoint", "http://127.0.0.1:8080", "Marathon server endpoint. You can also specify multiple endpoint for Marathon") + traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Domain, "marathon.domain", "", "Default domain used") + traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.NetworkInterface, "marathon.networkInterface", "eth0", "Network interface used to call Marathon web services. Needed in case of multiple network interfaces") + + traefikCmd.PersistentFlags().BoolVar(&arguments.consul, "consul", false, "Enable Consul backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.Consul.Watch, "consul.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Filename, "consul.filename", "", "Override default configuration template. For advanced users :)") + traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Endpoint, "consul.endpoint", "127.0.0.1:8500", "Consul server endpoint") + traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Prefix, "consul.prefix", "/traefik", "Prefix used for KV store") + + traefikCmd.PersistentFlags().BoolVar(&arguments.zookeeper, "zookeeper", false, "Enable Zookeeper backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.Zookeeper.Watch, "zookeeper.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Filename, "zookeeper.filename", "", "Override default configuration template. For advanced users :)") + traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Endpoint, "zookeeper.endpoint", "127.0.0.1:2181", "Zookeeper server endpoint") + traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Prefix, "zookeeper.prefix", "/traefik", "Prefix used for KV store") + + traefikCmd.PersistentFlags().BoolVar(&arguments.etcd, "etcd", false, "Enable Etcd backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.Etcd.Watch, "etcd.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Filename, "etcd.filename", "", "Override default configuration template. For advanced users :)") + traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Endpoint, "etcd.endpoint", "127.0.0.1:4001", "Etcd server endpoint") + traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Prefix, "etcd.prefix", "/traefik", "Prefix used for KV store") + + traefikCmd.PersistentFlags().BoolVar(&arguments.boltdb, "boltdb", false, "Enable Boltdb backend") + traefikCmd.PersistentFlags().BoolVar(&arguments.Boltdb.Watch, "boltdb.watch", true, "Watch provider") + traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Filename, "boltdb.filename", "", "Override default configuration template. For advanced users :)") + traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Endpoint, "boltdb.endpoint", "127.0.0.1:4001", "Boltdb server endpoint") + traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store") + + 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("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.SetDefault("certificates", &Certificates{}) + viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second)) +} + +func run() { + fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags) + + // load global configuration + globalConfiguration := LoadConfiguration() + + loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile) + defer loggerMiddleware.Close() + + // logging + level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) + if err != nil { + log.Fatal("Error getting level", err) + } + log.SetLevel(level) + + if len(globalConfiguration.TraefikLogsFile) > 0 { + fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + defer fi.Close() + if err != nil { + log.Fatal("Error opening file", err) + } else { + log.SetOutput(fi) + log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) + } + } else { + log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) + } + log.Debugf("Global configuration loaded %+v", globalConfiguration) + server := NewServer(*globalConfiguration) + server.Start() + defer server.Close() + log.Info("Shutting down") +} diff --git a/configuration.go b/configuration.go index 92dadf383..14b74ae87 100644 --- a/configuration.go +++ b/configuration.go @@ -4,9 +4,13 @@ import ( fmtlog "log" "time" - "github.com/BurntSushi/toml" + "fmt" "github.com/emilevauge/traefik/provider" "github.com/emilevauge/traefik/types" + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" + "github.com/wendal/errors" + "strings" ) // GlobalConfiguration holds global configuration (with providers, etc.). @@ -16,7 +20,7 @@ type GlobalConfiguration struct { GraceTimeOut int64 AccessLogsFile string TraefikLogsFile string - Certificates []Certificate + Certificates Certificates LogLevel string ProvidersThrottleDuration time.Duration Docker *provider.Docker @@ -29,6 +33,38 @@ type GlobalConfiguration struct { Boltdb *provider.BoltDb } +// Certificates defines traefik certificates type +type Certificates []Certificate + +// 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 (certs *Certificates) String() string { + if len(*certs) == 0 { + return "" + } + return (*certs)[0].CertFile + "," + (*certs)[0].KeyFile +} + +// 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 (certs *Certificates) Set(value string) error { + files := strings.Split(value, ",") + if len(files) != 2 { + return errors.New("Bad certificates format: " + value) + } + *certs = append(*certs, Certificate{ + CertFile: files[0], + KeyFile: files[1], + }) + return nil +} + +// Type is type of the struct +func (certs *Certificates) Type() string { + return fmt.Sprint("certificates") +} + // Certificate holds a SSL cert/key pair type Certificate struct { CertFile string @@ -47,13 +83,80 @@ func NewGlobalConfiguration() *GlobalConfiguration { return globalConfiguration } -// LoadFileConfig returns a GlobalConfiguration from reading the specified file (a toml file). -func LoadFileConfig(file string) *GlobalConfiguration { +// LoadConfiguration returns a GlobalConfiguration. +func LoadConfiguration() *GlobalConfiguration { configuration := NewGlobalConfiguration() - if _, err := toml.DecodeFile(file, configuration); err != nil { + viper.SetEnvPrefix("traefik") + viper.SetConfigType("toml") + viper.AutomaticEnv() + if len(viper.GetString("configFile")) > 0 { + viper.SetConfigFile(viper.GetString("configFile")) + } else { + viper.SetConfigName("traefik") // name of config file (without extension) + } + viper.AddConfigPath("/etc/traefik/") // path to look for the config file in + viper.AddConfigPath("$HOME/.traefik") // call multiple times to add many search paths + viper.AddConfigPath(".") // optionally look for config in the working directory + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file fmtlog.Fatalf("Error reading file: %s", err) } + if len(arguments.Certificates) > 0 { + viper.Set("certificates", arguments.Certificates) + } + if arguments.web { + viper.Set("web", arguments.Web) + } + if arguments.file { + viper.Set("file", arguments.File) + } + if !arguments.dockerTLS { + arguments.Docker.TLS = nil + } + if arguments.docker { + viper.Set("docker", arguments.Docker) + } + if arguments.marathon { + viper.Set("marathon", arguments.Marathon) + } + if arguments.consul { + viper.Set("consul", arguments.Consul) + } + if arguments.zookeeper { + viper.Set("zookeeper", arguments.Zookeeper) + } + if arguments.etcd { + viper.Set("etcd", arguments.Etcd) + } + if arguments.boltdb { + viper.Set("boltdb", arguments.Boltdb) + } + err = unmarshal(&configuration) + if err != nil { + fmtlog.Fatalf("Error reading file: %s", err) + } + return configuration } +func unmarshal(rawVal interface{}) error { + config := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.StringToTimeDurationHookFunc(), + Metadata: nil, + Result: rawVal, + WeaklyTypedInput: true, + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + err = decoder.Decode(viper.AllSettings()) + if err != nil { + return err + } + return nil +} + type configs map[string]*types.Configuration diff --git a/glide.yaml b/glide.yaml index 65400739f..8143a1975 100644 --- a/glide.yaml +++ b/glide.yaml @@ -145,9 +145,13 @@ import: - package: github.com/donovanhide/eventsource ref: d8a3071799b98cacd30b6da92f536050ccfe6da4 - package: github.com/golang/glog - ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f + ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f - package: github.com/spf13/cast - version: ee7b3e0353166ab1f3a605294ac8cd2b77953778 + ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778 - package: github.com/spf13/viper - version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 + ref: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 + - package: github.com/spf13/cobra + subpackages: + - /cobra + diff --git a/integration/basic_test.go b/integration/basic_test.go index cb47be0a0..d9a9178a8 100644 --- a/integration/basic_test.go +++ b/integration/basic_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" ) @@ -18,10 +18,10 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { output, err := cmd.CombinedOutput() c.Assert(err, checker.NotNil) - c.Assert(string(output), checker.Contains, "Error reading file: open traefik.toml: no such file or directory") + c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory") nonExistentFile := "non/existent/file.toml" - cmd = exec.Command(traefikBinary, nonExistentFile) + cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile) output, err = cmd.CombinedOutput() c.Assert(err, checker.NotNil) @@ -29,30 +29,30 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { } func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/invalid_configuration.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml") output, err := cmd.CombinedOutput() c.Assert(err, checker.NotNil) - c.Assert(string(output), checker.Contains, "Error reading file: Near line 1") + c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1") } func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/simple_default.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/simple_default.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() time.Sleep(500 * time.Millisecond) // TODO validate : run on 80 - resp, err := http.Get("http://127.0.0.1/") + resp, err := http.Get("http://127.0.0.1:8000/") - // Expected a 404 as we did not comfigure anything + // Expected a 404 as we did not configure anything c.Assert(err, checker.IsNil) c.Assert(resp.StatusCode, checker.Equals, 404) } func (s *SimpleSuite) TestWithWebConfig(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/simple_web.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/simple_web.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() diff --git a/integration/consul_test.go b/integration/consul_test.go index 634196ae8..07af1aa7f 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -10,14 +10,14 @@ import ( ) func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/consul/simple.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/consul/simple.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() time.Sleep(500 * time.Millisecond) // TODO validate : run on 80 - resp, err := http.Get("http://127.0.0.1/") + resp, err := http.Get("http://127.0.0.1:8000/") // Expected a 404 as we did not comfigure anything c.Assert(err, checker.IsNil) diff --git a/integration/docker_test.go b/integration/docker_test.go index 0f11049bf..884e7dd4b 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -139,14 +139,14 @@ func (s *DockerSuite) TestSimpleConfiguration(c *check.C) { file := s.adaptFileForHost(c, "fixtures/docker/simple.toml") defer os.Remove(file) - cmd := exec.Command(traefikBinary, file) + cmd := exec.Command(traefikBinary, "--configFile="+file) err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() time.Sleep(500 * time.Millisecond) // TODO validate : run on 80 - resp, err := http.Get("http://127.0.0.1/") + resp, err := http.Get("http://127.0.0.1:8000/") c.Assert(err, checker.IsNil) // Expected a 404 as we did not comfigure anything @@ -159,7 +159,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) { name := s.startContainer(c, "swarm:1.0.0", "manage", "token://blablabla") // Start traefik - cmd := exec.Command(traefikBinary, file) + cmd := exec.Command(traefikBinary, "--configFile="+file) err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() @@ -168,7 +168,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) { time.Sleep(1500 * time.Millisecond) client := &http.Client{} - req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil) + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil) c.Assert(err, checker.IsNil) req.Host = fmt.Sprintf("%s.docker.localhost", name) resp, err := client.Do(req) @@ -196,7 +196,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) { s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla") // Start traefik - cmd := exec.Command(traefikBinary, file) + cmd := exec.Command(traefikBinary, "--configFile="+file) err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() @@ -205,7 +205,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) { time.Sleep(1500 * time.Millisecond) client := &http.Client{} - req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil) + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil) c.Assert(err, checker.IsNil) req.Host = fmt.Sprintf("my.super.host") resp, err := client.Do(req) @@ -232,7 +232,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) { s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla") // Start traefik - cmd := exec.Command(traefikBinary, file) + cmd := exec.Command(traefikBinary, "--configFile="+file) err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() @@ -241,7 +241,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) { time.Sleep(1500 * time.Millisecond) client := &http.Client{} - req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil) + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil) c.Assert(err, checker.IsNil) req.Host = fmt.Sprintf("my.super.host") resp, err := client.Do(req) diff --git a/integration/file_test.go b/integration/file_test.go index 7fbed2390..8d32dae63 100644 --- a/integration/file_test.go +++ b/integration/file_test.go @@ -10,13 +10,13 @@ import ( ) func (s *FileSuite) TestSimpleConfiguration(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/file/simple.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/file/simple.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() time.Sleep(1000 * time.Millisecond) - resp, err := http.Get("http://127.0.0.1/") + resp, err := http.Get("http://127.0.0.1:8000/") // Expected a 404 as we did not configure anything c.Assert(err, checker.IsNil) @@ -25,13 +25,13 @@ func (s *FileSuite) TestSimpleConfiguration(c *check.C) { // #56 regression test, make sure it does not fail func (s *FileSuite) TestSimpleConfigurationNoPanic(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/file/56-simple-panic.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/file/56-simple-panic.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() time.Sleep(1000 * time.Millisecond) - resp, err := http.Get("http://127.0.0.1/") + resp, err := http.Get("http://127.0.0.1:8000/") // Expected a 404 as we did not configure anything c.Assert(err, checker.IsNil) diff --git a/integration/fixtures/consul/simple.toml b/integration/fixtures/consul/simple.toml index 02707a085..34947f7e8 100644 --- a/integration/fixtures/consul/simple.toml +++ b/integration/fixtures/consul/simple.toml @@ -4,6 +4,7 @@ # Default: ":80" # # port = ":80" +port = ":8000" # # LogLevel logLevel = "DEBUG" diff --git a/integration/fixtures/docker/simple.toml b/integration/fixtures/docker/simple.toml index 8968895c0..963cfd5a8 100644 --- a/integration/fixtures/docker/simple.toml +++ b/integration/fixtures/docker/simple.toml @@ -4,6 +4,7 @@ # Default: ":80" # # port = ":80" +port = ":8000" # # LogLevel logLevel = "DEBUG" diff --git a/integration/fixtures/file/56-simple-panic.toml b/integration/fixtures/file/56-simple-panic.toml index 100b58a93..f8c792e65 100644 --- a/integration/fixtures/file/56-simple-panic.toml +++ b/integration/fixtures/file/56-simple-panic.toml @@ -4,6 +4,7 @@ # Default: ":80" # # port = ":80" +port = ":8000" # # LogLevel logLevel = "DEBUG" diff --git a/integration/fixtures/file/simple.toml b/integration/fixtures/file/simple.toml index 6d799d24b..3db190f40 100644 --- a/integration/fixtures/file/simple.toml +++ b/integration/fixtures/file/simple.toml @@ -4,6 +4,7 @@ # Default: ":80" # # port = ":80" +port = ":8000" # # LogLevel logLevel = "DEBUG" diff --git a/integration/fixtures/https/https_sni.toml b/integration/fixtures/https/https_sni.toml index 6af20c15f..aebf22657 100644 --- a/integration/fixtures/https/https_sni.toml +++ b/integration/fixtures/https/https_sni.toml @@ -1,4 +1,4 @@ -port = ":443" +port = ":4443" logLevel = "DEBUG" [[certificates]] diff --git a/integration/fixtures/marathon/simple.toml b/integration/fixtures/marathon/simple.toml index 1e97650a6..c6cfbb05a 100644 --- a/integration/fixtures/marathon/simple.toml +++ b/integration/fixtures/marathon/simple.toml @@ -4,6 +4,7 @@ # Default: ":80" # # port = ":80" +port = ":8000" # # LogLevel logLevel = "DEBUG" diff --git a/integration/fixtures/simple_default.toml b/integration/fixtures/simple_default.toml index ac7dee9f1..3f5fe41df 100644 --- a/integration/fixtures/simple_default.toml +++ b/integration/fixtures/simple_default.toml @@ -3,7 +3,7 @@ # Optional # Default: ":80" # -# port = ":80" +port = ":8000" # # LogLevel logLevel = "DEBUG" diff --git a/integration/fixtures/simple_web.toml b/integration/fixtures/simple_web.toml index e88dd3049..49452f6e5 100644 --- a/integration/fixtures/simple_web.toml +++ b/integration/fixtures/simple_web.toml @@ -1,3 +1,4 @@ +port = ":8000" logLevel = "DEBUG" [web] diff --git a/integration/https_test.go b/integration/https_test.go index 9a81609e0..41b89e243 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -19,7 +19,7 @@ type HTTPSSuite struct{ BaseSuite } // "snitest.com", which happens to match the CN of 'snitest.com.crt'. The test // verifies that traefik presents the correct certificate. func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/https/https_sni.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() @@ -30,7 +30,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) { InsecureSkipVerify: true, ServerName: "snitest.com", } - conn, err := tls.Dial("tcp", "127.0.0.1:443", tlsConfig) + conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) defer conn.Close() @@ -46,7 +46,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) { // SNI hostnames of "snitest.org" and "snitest.com". The test verifies // that traefik routes the requests to the expected backends. func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/https/https_sni.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() @@ -72,7 +72,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { } client := &http.Client{Transport: tr1} - req, _ := http.NewRequest("GET", "https://127.0.0.1/", nil) + req, _ := http.NewRequest("GET", "https://127.0.0.1:4443/", nil) req.Host = "snitest.com" req.Header.Set("Host", "snitest.com") req.Header.Set("Accept", "*/*") @@ -82,7 +82,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { c.Assert(resp.StatusCode, checker.Equals, 204) client = &http.Client{Transport: tr2} - req, _ = http.NewRequest("GET", "https://127.0.0.1/", nil) + req, _ = http.NewRequest("GET", "https://127.0.0.1:4443/", nil) req.Host = "snitest.org" req.Header.Set("Host", "snitest.org") req.Header.Set("Accept", "*/*") diff --git a/integration/marathon_test.go b/integration/marathon_test.go index 0f10f7252..40a42ffd6 100644 --- a/integration/marathon_test.go +++ b/integration/marathon_test.go @@ -10,14 +10,14 @@ import ( ) func (s *MarathonSuite) TestSimpleConfiguration(c *check.C) { - cmd := exec.Command(traefikBinary, "fixtures/marathon/simple.toml") + cmd := exec.Command(traefikBinary, "--configFile=fixtures/marathon/simple.toml") err := cmd.Start() c.Assert(err, checker.IsNil) defer cmd.Process.Kill() time.Sleep(500 * time.Millisecond) // TODO validate : run on 80 - resp, err := http.Get("http://127.0.0.1/") + resp, err := http.Get("http://127.0.0.1:8000/") // Expected a 404 as we did not configure anything c.Assert(err, checker.IsNil) diff --git a/provider/boltdb.go b/provider/boltdb.go index 9304f8a8f..fec8c3654 100644 --- a/provider/boltdb.go +++ b/provider/boltdb.go @@ -8,13 +8,13 @@ import ( // BoltDb holds configurations of the BoltDb provider. type BoltDb struct { - Kv + Kv `mapstructure:",squash"` } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage) error { - provider.StoreType = store.BOLTDB + provider.storeType = store.BOLTDB boltdb.Register() return provider.provide(configurationChan) } diff --git a/provider/consul.go b/provider/consul.go index fa0be91cf..49d3031ac 100644 --- a/provider/consul.go +++ b/provider/consul.go @@ -8,13 +8,13 @@ import ( // Consul holds configurations of the Consul provider. type Consul struct { - Kv + Kv `mapstructure:",squash"` } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage) error { - provider.StoreType = store.CONSUL + provider.storeType = store.CONSUL consul.Register() return provider.provide(configurationChan) } diff --git a/provider/docker.go b/provider/docker.go index 2d8ea65a8..e4e6a1f5b 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -17,10 +17,10 @@ import ( // Docker holds configurations of the Docker provider. type Docker struct { - baseProvider - Endpoint string - Domain string - TLS *DockerTLS + BaseProvider `mapstructure:",squash"` + Endpoint string + Domain string + TLS *DockerTLS } // DockerTLS holds TLS specific configurations diff --git a/provider/etcd.go b/provider/etcd.go index d51ecc380..21455b155 100644 --- a/provider/etcd.go +++ b/provider/etcd.go @@ -8,13 +8,13 @@ import ( // Etcd holds configurations of the Etcd provider. type Etcd struct { - Kv + Kv `mapstructure:",squash"` } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage) error { - provider.StoreType = store.ETCD + provider.storeType = store.ETCD etcd.Register() return provider.provide(configurationChan) } diff --git a/provider/file.go b/provider/file.go index c5ef28c00..0b3bb0cdd 100644 --- a/provider/file.go +++ b/provider/file.go @@ -13,7 +13,7 @@ import ( // File holds configurations of the File provider. type File struct { - baseProvider + BaseProvider `mapstructure:",squash"` } // Provide allows the provider to provide configurations to traefik diff --git a/provider/kv.go b/provider/kv.go index d610e666d..4fc5cb143 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -15,16 +15,16 @@ import ( // Kv holds common configurations of key-value providers. type Kv struct { - baseProvider - Endpoint string - Prefix string - StoreType store.Backend - kvclient store.Store + BaseProvider `mapstructure:",squash"` + Endpoint string + Prefix string + storeType store.Backend + kvclient store.Store } func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error { kv, err := libkv.NewStore( - provider.StoreType, + provider.storeType, []string{provider.Endpoint}, &store.Config{ ConnectionTimeout: 30 * time.Second, @@ -50,7 +50,7 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error configuration := provider.loadConfig() if configuration != nil { configurationChan <- types.ConfigMessage{ - ProviderName: string(provider.StoreType), + ProviderName: string(provider.storeType), Configuration: configuration, } } @@ -60,7 +60,7 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error } configuration := provider.loadConfig() configurationChan <- types.ConfigMessage{ - ProviderName: string(provider.StoreType), + ProviderName: string(provider.storeType), Configuration: configuration, } return nil diff --git a/provider/marathon.go b/provider/marathon.go index a2c8f5532..c2fc96f25 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -14,7 +14,7 @@ import ( // Marathon holds configuration of the Marathon provider. type Marathon struct { - baseProvider + BaseProvider `mapstructure:",squash"` Endpoint string Domain string NetworkInterface string diff --git a/provider/provider.go b/provider/provider.go index d2ceb5ebc..78087da9e 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -18,12 +18,13 @@ type Provider interface { Provide(configurationChan chan<- types.ConfigMessage) error } -type baseProvider struct { +// BaseProvider should be inherited by providers +type BaseProvider struct { Watch bool Filename string } -func (p *baseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { +func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { var ( buf []byte err error diff --git a/provider/provider_test.go b/provider/provider_test.go index d9578fee2..490a82a7b 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -9,7 +9,7 @@ import ( ) type myProvider struct { - baseProvider + BaseProvider } func (p *myProvider) Foo() string { @@ -49,7 +49,7 @@ func TestConfigurationErrors(t *testing.T) { }{ { provider: &myProvider{ - baseProvider{ + BaseProvider{ Filename: "/non/existent/template.tmpl", }, }, @@ -62,7 +62,7 @@ func TestConfigurationErrors(t *testing.T) { }, { provider: &myProvider{ - baseProvider{ + BaseProvider{ Filename: templateErrorFile.Name(), }, }, @@ -70,7 +70,7 @@ func TestConfigurationErrors(t *testing.T) { }, { provider: &myProvider{ - baseProvider{ + BaseProvider{ Filename: templateInvalidTOMLFile.Name(), }, }, @@ -125,7 +125,7 @@ func TestGetConfiguration(t *testing.T) { } provider := &myProvider{ - baseProvider{ + BaseProvider{ Filename: templateFile.Name(), }, } diff --git a/provider/zk.go b/provider/zk.go index 2f379aa7a..1dfc0fa7b 100644 --- a/provider/zk.go +++ b/provider/zk.go @@ -14,7 +14,7 @@ type Zookepper struct { // Provide allows the provider to provide configurations to traefik // using the given configuration channel. func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage) error { - provider.StoreType = store.ZK + provider.storeType = store.ZK zookeeper.Register() return provider.provide(configurationChan) } diff --git a/server.go b/server.go index d273d3d43..5b70fa1fa 100644 --- a/server.go +++ b/server.go @@ -16,6 +16,7 @@ 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" @@ -26,6 +27,8 @@ import ( "time" ) +var oxyLogger = &OxyLogger{} + // Server is the reverse-proxy/load-balancer engine type Server struct { srv *manners.GracefulServer @@ -89,11 +92,11 @@ func (server *Server) Stop() { // Close destroys the server func (server *Server) Close() { - defer close(server.configurationChan) - defer close(server.configurationChanValidated) - defer close(server.sigs) - defer close(server.stopChan) - defer server.loggerMiddleware.Close() + close(server.configurationChan) + close(server.configurationChanValidated) + close(server.sigs) + close(server.stopChan) + server.loggerMiddleware.Close() } func (server *Server) listenProviders() { @@ -101,18 +104,18 @@ func (server *Server) listenProviders() { lastConfigs := make(map[string]*types.ConfigMessage) for { configMsg := <-server.configurationChan - log.Infof("Configuration receveived from provider %s: %#v", configMsg.ProviderName, configMsg.Configuration) + log.Debugf("Configuration receveived from provider %s: %#v", configMsg.ProviderName, configMsg.Configuration) lastConfigs[configMsg.ProviderName] = &configMsg if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) { - log.Infof("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 server.configurationChanValidated <- configMsg } else { - log.Infof("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() { <-time.After(server.globalConfiguration.ProvidersThrottleDuration) if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) { - log.Infof("Waited for %s config, OK", configMsg.ProviderName) + log.Debugf("Waited for %s config, OK", configMsg.ProviderName) server.configurationChanValidated <- *lastConfigs[configMsg.ProviderName] } }() @@ -172,7 +175,7 @@ func (server *Server) configureProviders() { if server.globalConfiguration.File != nil { if len(server.globalConfiguration.File.Filename) == 0 { // no filename, setting to global config file - server.globalConfiguration.File.Filename = *globalConfigFile + server.globalConfiguration.File.Filename = viper.GetString("configFile") } server.providers = append(server.providers, server.globalConfiguration.File) } @@ -298,11 +301,11 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo backends := map[string]http.Handler{} for _, configuration := range configurations { for frontendName, frontend := range configuration.Frontends { - log.Debugf("Creating frontend %s", frontendName) + log.Infof("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.Debugf("Creating route %s %s:%s", routeName, route.Rule, route.Value) + 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 @@ -310,7 +313,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo newRoute = newRouteReflect[0].Interface().(*mux.Route) } if backends[frontend.Backend] == nil { - log.Debugf("Creating backend %s", frontend.Backend) + log.Infof("Creating backend %s", frontend.Backend) var lb http.Handler rr, _ := roundrobin.New(fwd) if configuration.Backends[frontend.Backend] == nil { @@ -341,7 +344,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo if err != nil { return nil, err } - log.Infof("Creating server %s %s", serverName, url.String()) + log.Infof("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight) rr.UpsertServer(url, roundrobin.Weight(server.Weight)) } } diff --git a/traefik.go b/traefik.go index 562fd3a62..ea5ef7d62 100644 --- a/traefik.go +++ b/traefik.go @@ -1,74 +1,17 @@ package main import ( - "errors" fmtlog "log" "os" - "reflect" "runtime" - "strings" - - log "github.com/Sirupsen/logrus" - "github.com/emilevauge/traefik/middlewares" - "github.com/thoas/stats" - "gopkg.in/alecthomas/kingpin.v2" -) - -var ( - globalConfigFile = kingpin.Arg("conf", "Main configration file.").Default("traefik.toml").String() - version = kingpin.Flag("version", "Get Version.").Short('v').Bool() - metrics = stats.New() - oxyLogger = &OxyLogger{} ) func main() { runtime.GOMAXPROCS(runtime.NumCPU()) - kingpin.Version(Version + " built on the " + BuildDate) - kingpin.Parse() - fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags) - // load global configuration - globalConfiguration := LoadFileConfig(*globalConfigFile) - - loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile) - defer loggerMiddleware.Close() - - // logging - level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) - if err != nil { - log.Fatal("Error getting level", err) + if err := traefikCmd.Execute(); err != nil { + fmtlog.Println(err) + os.Exit(-1) } - log.SetLevel(level) - - if len(globalConfiguration.TraefikLogsFile) > 0 { - fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - defer fi.Close() - if err != nil { - log.Fatal("Error opening file", err) - } else { - log.SetOutput(fi) - log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true}) - } - } else { - log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) - } - log.Debugf("Global configuration loaded %+v", globalConfiguration) - server := NewServer(*globalConfiguration) - server.Start() - server.Close() - log.Info("Shutting down") -} - -// Invoke calls the specified method with the specified arguments on the specified interface. -// It uses the go(lang) reflect package. -func invoke(any interface{}, name string, args ...interface{}) ([]reflect.Value, error) { - inputs := make([]reflect.Value, len(args)) - for i := range args { - inputs[i] = reflect.ValueOf(args[i]) - } - method := reflect.ValueOf(any).MethodByName(name) - if method.IsValid() { - return method.Call(inputs), nil - } - return nil, errors.New("Method not found: " + name) + os.Exit(0) } diff --git a/traefik.sample.toml b/traefik.sample.toml index 020bc4acf..bf5a7b7fc 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -78,6 +78,11 @@ # # CertFile = "traefik.crt" # KeyFile = "traefik.key" +# +# Set REST API to read-only mode +# +# Optional +# ReadOnly = false ################################################################ diff --git a/utils.go b/utils.go new file mode 100644 index 000000000..bed6ae7f8 --- /dev/null +++ b/utils.go @@ -0,0 +1,23 @@ +/* +Copyright +*/ +package main + +import ( + "errors" + "reflect" +) + +// Invoke calls the specified method with the specified arguments on the specified interface. +// It uses the go(lang) reflect package. +func invoke(any interface{}, name string, args ...interface{}) ([]reflect.Value, error) { + inputs := make([]reflect.Value, len(args)) + for i := range args { + inputs[i] = reflect.ValueOf(args[i]) + } + method := reflect.ValueOf(any).MethodByName(name) + if method.IsValid() { + return method.Call(inputs), nil + } + return nil, errors.New("Method not found: " + name) +} diff --git a/web.go b/web.go index 3c4eb91a1..6d1f7d3fe 100644 --- a/web.go +++ b/web.go @@ -11,9 +11,12 @@ import ( "github.com/emilevauge/traefik/autogen" "github.com/emilevauge/traefik/types" "github.com/gorilla/mux" + "github.com/thoas/stats" "github.com/unrolled/render" ) +var metrics = stats.New() + // WebProvider is a provider.Provider implementation that provides the UI. // FIXME to be handled another way. type WebProvider struct {