diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ecdac0319..8772fa828 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -77,6 +77,23 @@ ok github.com/containous/traefik 0.005s coverage: 4.1% of statements Test success ``` +For development purpose, you can specifiy which tests to run by using: +``` +# Run every tests in the MyTest suite +TESTFLAGS="-check.f MyTestSuite" make test-integration + +# Run the test "MyTest" in the MyTest suite +TESTFLAGS="-check.f MyTestSuite.MyTest" make test-integration + +# Run every tests starting with "My", in the MyTest suite +TESTFLAGS="-check.f MyTestSuite.My" make test-integration + +# Run every tests ending with "Test", in the MyTest suite +TESTFLAGS="-check.f MyTestSuite.*Test" make test-integration +``` + +More: https://labix.org/gocheck + ### Documentation The [documentation site](http://docs.traefik.io/) is built with [mkdocs](http://mkdocs.org/) diff --git a/.travis.yml b/.travis.yml index 69353d4c6..adcc5e2c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,9 @@ branches: - except: - - /^v\d\.\d\.\d.*$/ env: global: - secure: btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvskjrP0t/BFaS4yxIURjnFRn+ugQIEa0pLspB9UJArW/vgOSpIWM9/OQ/fg8z5XuMxN6Md4DL1/iLypMNSageA1x0TRdt89+D1N1dALpg5XRCXLFbC84TLi0gjlFuib9ibPKzEhLT+anCRJ6iZMzeupDSoaCVbAtJMoDvXw4+4AcRZ1+k4MybBLyCib5boaEOt4pTT88mz4Kk0YaMwPVJyg9Qv36VqyUcPS09Yd95LuyVQ4+tZt8Y1ccbIzULsK+sLM3hLCzxlmlpN3dQBlZJiiRtQde0mgGAKyC0P0A1XjuDTywcsa5edB+fTk1Dsewz9xZ9V0NmMz8t+UNZnaSsAPga9i86jULbXUUwMVSzVRc+Xgx02liB/8qI1xYC9FM6ilStt7rn7mF0k3KbiWhcptgeXjO6Lah9FjEKd5w4MXsdUSTi/86rQaLo+kj+XdaTrXCTulKHyRyQEUj+8V1w0oVz7pcGjePHd7y5oU9ByifVQy6sytuFBfRZvugM5bKHo+i0pcWvixrZS42DrzwxZJsspANOvqSe5ifVbvOkfUppQdCBIwptxV5N1b49XPKU3W/w34QJ8xGmKp3TFA7WwVCztriFHjPgiRpB3EG99Bg= - REPO: $TRAVIS_REPO_SLUG - - VERSION: v1.0.0-beta.$TRAVIS_BUILD_NUMBER + - VERSION: $TRAVIS_TAG matrix: - DOCKER_VERSION=1.9.1 - DOCKER_VERSION=1.10.1 @@ -17,6 +15,7 @@ install: - sudo curl https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION} -o /usr/bin/docker - sudo chmod +x /usr/bin/docker - sudo service docker start +- sleep 5 - docker version - pip install --user mkdocs - pip install --user pymdown-extensions @@ -30,3 +29,4 @@ script: - make image after_success: - make deploy +- make deploy-pr diff --git a/Makefile b/Makefile index 6dc75de35..8ca374210 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ REPONAME := $(shell echo $(REPO) | tr '[:upper:]' '[:lower:]') TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"containous/traefik") INTEGRATION_OPTS := $(if $(MAKE_DOCKER_HOST),-e "DOCKER_HOST=$(MAKE_DOCKER_HOST)", -v "/var/run/docker.sock:/var/run/docker.sock") +DOCKER_BUILD_ARGS := $(if $(DOCKER_VERSION), "--build-arg=DOCKER_VERSION=$(DOCKER_VERSION)",) DOCKER_RUN_TRAEFIK := docker run $(INTEGRATION_OPTS) -it $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)" print-%: ; @echo $*=$($*) @@ -46,7 +47,7 @@ validate: build ## validate gofmt, golint and go vet $(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint build: dist - docker build --build-arg=DOCKER_VERSION=${DOCKER_VERSION} -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile . + docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile . build-webui: docker build -t traefik-webui -f webui/Dockerfile webui @@ -84,5 +85,8 @@ fmt: deploy: ./script/deploy.sh +deploy-pr: + ./script/deploy-pr.sh + help: ## this help @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 684f164fc..e16bb35a9 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ You can access to a simple HTML frontend of Træfik. - The simple way: grab the latest binary from the [releases](https://github.com/containous/traefik/releases) page and just run it with the [sample configuration file](https://raw.githubusercontent.com/containous/traefik/master/traefik.sample.toml): ```shell -./traefik -c traefik.toml +./traefik --configFile=traefik.toml ``` - Use the tiny Docker image: diff --git a/acme/acme.go b/acme/acme.go index 53c1bd123..99e340ae4 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -16,6 +16,7 @@ import ( fmtlog "log" "os" "reflect" + "strings" "sync" "time" ) @@ -161,15 +162,50 @@ func (dc *DomainsCertificate) needRenew() bool { // ACME allows to connect to lets encrypt and retrieve certs type ACME struct { - Email string - Domains []Domain - StorageFile string - OnDemand bool - CAServer string - EntryPoint string + Email string `description:"Email address used for registration"` + Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` + StorageFile string `description:"File used for certificates storage."` + OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` + CAServer string `description:"CA server to use."` + EntryPoint string `description:"Entrypoint to proxy acme challenge to."` storageLock sync.RWMutex } +//Domains parse []Domain +type Domains []Domain + +//Set []Domain +func (ds *Domains) Set(str string) error { + fargs := func(c rune) bool { + return c == ',' || c == ';' + } + // get function + slice := strings.FieldsFunc(str, fargs) + if len(slice) < 1 { + return fmt.Errorf("Parse error ACME.Domain. Imposible to parse %s", str) + } + d := Domain{ + Main: slice[0], + SANs: []string{}, + } + if len(slice) > 1 { + d.SANs = slice[1:] + } + *ds = append(*ds, d) + return nil +} + +//Get []Domain +func (ds *Domains) Get() interface{} { return []Domain(*ds) } + +//String returns []Domain in string +func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) } + +//SetValue sets []Domain into the parser +func (ds *Domains) SetValue(val interface{}) { + *ds = Domains(val.([]Domain)) +} + // Domain holds a domain name with SANs type Domain struct { Main string diff --git a/acme/acme_test.go b/acme/acme_test.go new file mode 100644 index 000000000..302c5866e --- /dev/null +++ b/acme/acme_test.go @@ -0,0 +1,61 @@ +package acme + +import ( + "reflect" + "testing" +) + +func TestDomainsSet(t *testing.T) { + checkMap := map[string]Domains{ + "": {}, + "foo.com": {Domain{Main: "foo.com", SANs: []string{}}}, + "foo.com,bar.net": {Domain{Main: "foo.com", SANs: []string{"bar.net"}}}, + "foo.com,bar1.net,bar2.net,bar3.net": {Domain{Main: "foo.com", SANs: []string{"bar1.net", "bar2.net", "bar3.net"}}}, + } + for in, check := range checkMap { + ds := Domains{} + ds.Set(in) + if !reflect.DeepEqual(check, ds) { + t.Errorf("Expected %+v\nGo %+v", check, ds) + } + } +} + +func TestDomainsSetAppend(t *testing.T) { + inSlice := []string{ + "", + "foo1.com", + "foo2.com,bar.net", + "foo3.com,bar1.net,bar2.net,bar3.net", + } + checkSlice := []Domains{ + {}, + { + Domain{ + Main: "foo1.com", + SANs: []string{}}}, + { + Domain{ + Main: "foo1.com", + SANs: []string{}}, + Domain{ + Main: "foo2.com", + SANs: []string{"bar.net"}}}, + { + Domain{ + Main: "foo1.com", + SANs: []string{}}, + Domain{ + Main: "foo2.com", + SANs: []string{"bar.net"}}, + Domain{Main: "foo3.com", + SANs: []string{"bar1.net", "bar2.net", "bar3.net"}}}, + } + ds := Domains{} + for i, in := range inSlice { + ds.Set(in) + if !reflect.DeepEqual(checkSlice[i], ds) { + t.Errorf("Expected %s %+v\nGo %+v", in, checkSlice[i], ds) + } + } +} diff --git a/cmd.go b/cmd.go deleted file mode 100644 index f4240c4f4..000000000 --- a/cmd.go +++ /dev/null @@ -1,227 +0,0 @@ -/* -Copyright -*/ -package main - -import ( - "encoding/json" - fmtlog "log" - "os" - "strings" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/containous/traefik/middlewares" - "github.com/containous/traefik/provider" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "net/http" -) - -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 - consulTLS bool - consulCatalog bool - zookeeper bool - etcd bool - etcdTLS bool - boltdb bool - kubernetes bool -}{ - GlobalConfiguration{ - EntryPoints: make(EntryPoints), - Docker: &provider.Docker{ - TLS: &provider.DockerTLS{}, - }, - File: &provider.File{}, - Web: &WebProvider{}, - Marathon: &provider.Marathon{}, - Consul: &provider.Consul{ - Kv: provider.Kv{ - TLS: &provider.KvTLS{}, - }, - }, - ConsulCatalog: &provider.ConsulCatalog{}, - Zookeeper: &provider.Zookepper{}, - Etcd: &provider.Etcd{ - Kv: provider.Kv{ - TLS: &provider.KvTLS{}, - }, - }, - Boltdb: &provider.BoltDb{}, - Kubernetes: &provider.Kubernetes{}, - }, - false, - false, - false, - false, - 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("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.EntryPoints, "entryPoints", "Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'") - traefikCmd.PersistentFlags().Var(&arguments.DefaultEntryPoints, "defaultEntryPoints", "Entrypoints to be used by frontends that do not specify any entrypoint") - traefikCmd.PersistentFlags().StringP("logLevel", "l", "ERROR", "Log level") - traefikCmd.PersistentFlags().DurationVar(&arguments.ProvidersThrottleDuration, "providersThrottleDuration", time.Duration(2*time.Second), "Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time.") - traefikCmd.PersistentFlags().Int("maxIdleConnsPerHost", 0, "If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used") - - 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().BoolVar(&arguments.Marathon.ExposedByDefault, "marathon.exposedByDefault", true, "Expose Marathon apps by default") - - 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", "Comma sepparated Consul server endpoints") - traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Prefix, "consul.prefix", "/traefik", "Prefix used for KV store") - traefikCmd.PersistentFlags().BoolVar(&arguments.consulTLS, "consul.tls", false, "Enable Consul TLS support") - traefikCmd.PersistentFlags().StringVar(&arguments.Consul.TLS.CA, "consul.tls.ca", "", "TLS CA") - traefikCmd.PersistentFlags().StringVar(&arguments.Consul.TLS.Cert, "consul.tls.cert", "", "TLS cert") - traefikCmd.PersistentFlags().StringVar(&arguments.Consul.TLS.Key, "consul.tls.key", "", "TLS key") - traefikCmd.PersistentFlags().BoolVar(&arguments.Consul.TLS.InsecureSkipVerify, "consul.tls.insecureSkipVerify", false, "TLS insecure skip verify") - - traefikCmd.PersistentFlags().BoolVar(&arguments.consulCatalog, "consulCatalog", false, "Enable Consul catalog backend") - traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Domain, "consulCatalog.domain", "", "Default domain used") - traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Endpoint, "consulCatalog.endpoint", "127.0.0.1:8500", "Consul server endpoint") - traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Prefix, "consulCatalog.prefix", "traefik", "Consul catalog tag prefix") - - 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", "Comma sepparated Zookeeper server endpoints") - 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", "Comma sepparated Etcd server endpoints") - traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Prefix, "etcd.prefix", "/traefik", "Prefix used for KV store") - traefikCmd.PersistentFlags().BoolVar(&arguments.etcdTLS, "etcd.tls", false, "Enable Etcd TLS support") - traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.TLS.CA, "etcd.tls.ca", "", "TLS CA") - traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.TLS.Cert, "etcd.tls.cert", "", "TLS cert") - traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.TLS.Key, "etcd.tls.key", "", "TLS key") - traefikCmd.PersistentFlags().BoolVar(&arguments.Etcd.TLS.InsecureSkipVerify, "etcd.tls.insecureSkipVerify", false, "TLS insecure skip verify") - - 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") - - traefikCmd.PersistentFlags().BoolVar(&arguments.kubernetes, "kubernetes", false, "Enable Kubernetes backend") - traefikCmd.PersistentFlags().StringVar(&arguments.Kubernetes.Endpoint, "kubernetes.endpoint", "127.0.0.1:8080", "Kubernetes server endpoint") - traefikCmd.PersistentFlags().StringSliceVar(&arguments.Kubernetes.Namespaces, "kubernetes.namespaces", []string{}, "Kubernetes namespaces") - - _ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile")) - _ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut")) - _ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel")) - // TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105 - _ = viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration")) - _ = viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost")) - viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second)) - viper.SetDefault("logLevel", "ERROR") - viper.SetDefault("MaxIdleConnsPerHost", 200) -} - -func run() { - fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags) - - // load global configuration - globalConfiguration := LoadConfiguration() - - http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = globalConfiguration.MaxIdleConnsPerHost - 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 func() { - if err := fi.Close(); err != nil { - log.Error("Error closinf file", err) - } - }() - 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}) - } - jsonConf, _ := json.Marshal(globalConfiguration) - log.Debugf("Global configuration loaded %s", string(jsonConf)) - server := NewServer(*globalConfiguration) - server.Start() - defer server.Close() - log.Info("Shutting down") -} diff --git a/configuration.go b/configuration.go index 693b74c90..aa91a16f9 100644 --- a/configuration.go +++ b/configuration.go @@ -3,41 +3,45 @@ package main import ( "errors" "fmt" - fmtlog "log" - "regexp" - "strings" - "time" - "github.com/containous/traefik/acme" "github.com/containous/traefik/provider" "github.com/containous/traefik/types" - "github.com/mitchellh/mapstructure" - "github.com/spf13/viper" + "regexp" + "strings" + "time" ) +// TraefikConfiguration holds GlobalConfiguration and other stuff +type TraefikConfiguration struct { + GlobalConfiguration + ConfigFile string `short:"c" description:"Configuration file to use (TOML)."` +} + // GlobalConfiguration holds global configuration (with providers, etc.). // It's populated from the traefik configuration file passed as an argument to the binary. type GlobalConfiguration struct { - GraceTimeOut int64 - AccessLogsFile string - TraefikLogsFile string - LogLevel string - EntryPoints EntryPoints - ACME *acme.ACME - DefaultEntryPoints DefaultEntryPoints - ProvidersThrottleDuration time.Duration - MaxIdleConnsPerHost int - Retry *Retry - Docker *provider.Docker - File *provider.File - Web *WebProvider - Marathon *provider.Marathon - Consul *provider.Consul - ConsulCatalog *provider.ConsulCatalog - Etcd *provider.Etcd - Zookeeper *provider.Zookepper - Boltdb *provider.BoltDb - Kubernetes *provider.Kubernetes + GraceTimeOut int64 `short:"g" description:"Configuration file to use (TOML)."` + Debug bool `short:"d" description:"Enable debug mode"` + AccessLogsFile string `description:"Access logs file"` + TraefikLogsFile string `description:"Traefik logs file"` + LogLevel string `short:"l" description:"Log level"` + EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"` + Constraints types.Constraints `description:"Filter services by constraint, matching with service tags."` + ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL"` + DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint"` + ProvidersThrottleDuration time.Duration `description:"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."` + MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used"` + Retry *Retry `description:"Enable retry sending request if network error"` + Docker *provider.Docker `description:"Enable Docker backend"` + File *provider.File `description:"Enable File backend"` + Web *WebProvider `description:"Enable Web backend"` + Marathon *provider.Marathon `description:"Enable Marathon backend"` + Consul *provider.Consul `description:"Enable Consul backend"` + ConsulCatalog *provider.ConsulCatalog `description:"Enable Consul catalog backend"` + Etcd *provider.Etcd `description:"Enable Etcd backend"` + Zookeeper *provider.Zookepper `description:"Enable Zookeeper backend"` + Boltdb *provider.BoltDb `description:"Enable Boltdb backend"` + Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"` } // DefaultEntryPoints holds default entry points @@ -46,7 +50,7 @@ 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) + return strings.Join(*dep, ",") } // Set is the method to set the flag value, part of the flag.Value interface. @@ -63,9 +67,17 @@ func (dep *DefaultEntryPoints) Set(value string) error { return nil } +// Get return the EntryPoints map +func (dep *DefaultEntryPoints) Get() interface{} { return DefaultEntryPoints(*dep) } + +// SetValue sets the EntryPoints map with val +func (dep *DefaultEntryPoints) SetValue(val interface{}) { + *dep = DefaultEntryPoints(val.(DefaultEntryPoints)) +} + // Type is type of the struct func (dep *DefaultEntryPoints) Type() string { - return fmt.Sprint("defaultentrypoints²") + return fmt.Sprint("defaultentrypoints") } // EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...) @@ -74,7 +86,7 @@ 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 "" + return fmt.Sprintf("%+v", *ep) } // Set is the method to set the flag value, part of the flag.Value interface. @@ -121,9 +133,17 @@ func (ep *EntryPoints) Set(value string) error { return nil } +// Get return the EntryPoints map +func (ep *EntryPoints) Get() interface{} { return EntryPoints(*ep) } + +// SetValue sets the EntryPoints map with val +func (ep *EntryPoints) SetValue(val interface{}) { + *ep = EntryPoints(val.(EntryPoints)) +} + // Type is type of the struct func (ep *EntryPoints) Type() string { - return fmt.Sprint("entrypoints²") + return fmt.Sprint("entrypoints") } // EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...) @@ -186,121 +206,109 @@ type Certificate struct { // Retry contains request retry config type Retry struct { - Attempts int - MaxMem int64 + Attempts int `description:"Number of attempts"` + MaxMem int64 `description:"Maximum request body to be stored in memory in Mo"` } -// NewGlobalConfiguration returns a GlobalConfiguration with default values. -func NewGlobalConfiguration() *GlobalConfiguration { - return new(GlobalConfiguration) +// NewTraefikDefaultPointersConfiguration creates a TraefikConfiguration with pointers default values +func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { + //default Docker + var defaultDocker provider.Docker + defaultDocker.Watch = true + defaultDocker.Endpoint = "unix:///var/run/docker.sock" + defaultDocker.TLS = &provider.DockerTLS{} + + // default File + var defaultFile provider.File + defaultFile.Watch = true + defaultFile.Filename = "" //needs equivalent to viper.ConfigFileUsed() + + // default Web + var defaultWeb WebProvider + defaultWeb.Address = ":8080" + + // default Marathon + var defaultMarathon provider.Marathon + defaultMarathon.Watch = true + defaultMarathon.Endpoint = "http://127.0.0.1:8080" + defaultMarathon.ExposedByDefault = true + defaultMarathon.Constraints = []types.Constraint{} + + // default Consul + var defaultConsul provider.Consul + defaultConsul.Watch = true + defaultConsul.Endpoint = "127.0.0.1:8500" + defaultConsul.Prefix = "/traefik" + defaultConsul.TLS = &provider.KvTLS{} + defaultConsul.Constraints = []types.Constraint{} + + // default ConsulCatalog + var defaultConsulCatalog provider.ConsulCatalog + defaultConsulCatalog.Endpoint = "127.0.0.1:8500" + defaultConsulCatalog.Constraints = []types.Constraint{} + + // default Etcd + var defaultEtcd provider.Etcd + defaultEtcd.Watch = true + defaultEtcd.Endpoint = "127.0.0.1:400" + defaultEtcd.Prefix = "/traefik" + defaultEtcd.TLS = &provider.KvTLS{} + defaultEtcd.Constraints = []types.Constraint{} + + //default Zookeeper + var defaultZookeeper provider.Zookepper + defaultZookeeper.Watch = true + defaultZookeeper.Endpoint = "127.0.0.1:2181" + defaultZookeeper.Prefix = "/traefik" + defaultZookeeper.Constraints = []types.Constraint{} + + //default Boltdb + var defaultBoltDb provider.BoltDb + defaultBoltDb.Watch = true + defaultBoltDb.Endpoint = "127.0.0.1:4001" + defaultBoltDb.Prefix = "/traefik" + defaultBoltDb.Constraints = []types.Constraint{} + + //default Kubernetes + var defaultKubernetes provider.Kubernetes + defaultKubernetes.Watch = true + defaultKubernetes.Endpoint = "127.0.0.1:8080" + defaultKubernetes.Constraints = []types.Constraint{} + + defaultConfiguration := GlobalConfiguration{ + Docker: &defaultDocker, + File: &defaultFile, + Web: &defaultWeb, + Marathon: &defaultMarathon, + Consul: &defaultConsul, + ConsulCatalog: &defaultConsulCatalog, + Etcd: &defaultEtcd, + Zookeeper: &defaultZookeeper, + Boltdb: &defaultBoltDb, + Kubernetes: &defaultKubernetes, + Retry: &Retry{MaxMem: 2}, + } + return &TraefikConfiguration{ + GlobalConfiguration: defaultConfiguration, + } } -// LoadConfiguration returns a GlobalConfiguration. -func LoadConfiguration() *GlobalConfiguration { - configuration := NewGlobalConfiguration() - 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) +// NewTraefikConfiguration creates a TraefikConfiguration with default values +func NewTraefikConfiguration() *TraefikConfiguration { + return &TraefikConfiguration{ + GlobalConfiguration: GlobalConfiguration{ + GraceTimeOut: 10, + AccessLogsFile: "", + TraefikLogsFile: "", + LogLevel: "ERROR", + EntryPoints: map[string]*EntryPoint{}, + Constraints: []types.Constraint{}, + DefaultEntryPoints: []string{}, + ProvidersThrottleDuration: time.Duration(2 * time.Second), + MaxIdleConnsPerHost: 200, + }, + ConfigFile: "", } - 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 - if err := viper.ReadInConfig(); err != nil { - if len(viper.ConfigFileUsed()) > 0 { - fmtlog.Printf("Error reading configuration file: %s", err) - } else { - fmtlog.Printf("No configuration file found") - } - } - - if len(arguments.EntryPoints) > 0 { - viper.Set("entryPoints", arguments.EntryPoints) - } - if len(arguments.DefaultEntryPoints) > 0 { - viper.Set("defaultEntryPoints", arguments.DefaultEntryPoints) - } - if arguments.web { - viper.Set("web", arguments.Web) - } - 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.consulTLS { - arguments.Consul.TLS = nil - } - if arguments.consul { - viper.Set("consul", arguments.Consul) - } - if arguments.consulCatalog { - viper.Set("consulCatalog", arguments.ConsulCatalog) - } - if arguments.zookeeper { - viper.Set("zookeeper", arguments.Zookeeper) - } - if !arguments.etcdTLS { - arguments.Etcd.TLS = nil - } - if arguments.etcd { - viper.Set("etcd", arguments.Etcd) - } - if arguments.boltdb { - viper.Set("boltdb", arguments.Boltdb) - } - if arguments.kubernetes { - viper.Set("kubernetes", arguments.Kubernetes) - } - if err := unmarshal(&configuration); err != nil { - - 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 -} - -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/docs/benchmarks.md b/docs/benchmarks.md index 008f0e378..82d37a855 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -146,7 +146,7 @@ defaultEntryPoints = ["http"] ### whoami: ``` -wrk -t8 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-whoami:80/bench +wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-whoami:80/bench Running 1m test @ http://IP-whoami:80/bench 20 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev @@ -184,7 +184,7 @@ Transfer/sec: 4.97MB ### traefik: ``` -wrk -t8 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-traefik:8000/bench +wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-traefik:8000/bench Running 1m test @ http://IP-traefik:8000/bench 20 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev diff --git a/docs/toml.md b/docs/toml.md index ebf17ab71..b8cea8c7a 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -195,6 +195,51 @@ entryPoint = "https" main = "local4.com" ``` +## Constraints + +In a micro-service architecture, with a central service discovery, setting constraints limits Træfɪk scope to a smaller number of routes. + +Træfɪk filters services according to service attributes/tags set in your configuration backends. + +Supported backends: + +- Consul Catalog + +Supported filters: + +- ```tag``` + +``` +# Constraints definition + +# +# Optional +# + +# Simple matching constraint +# constraints = ["tag==api"] + +# Simple mismatching constraint +# constraints = ["tag!=api"] + +# Globbing +# constraints = ["tag==us-*"] + +# Backend-specific constraint +# [consulCatalog] +# endpoint = 127.0.0.1:8500 +# constraints = ["tag==api"] + +# Multiple constraints +# - "tag==" must match with at least one tag +# - "tag!=" must match with none of tags +# constraints = ["tag!=us-*", "tag!=asia-*"] +# [consulCatalog] +# endpoint = 127.0.0.1:8500 +# constraints = ["tag==api", "tag!=v*-beta"] +``` + + # Configuration backends ## File backend @@ -539,7 +584,8 @@ Labels can be used on containers to override default behaviour: - `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`). - `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend. - `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. -* `traefik.domain=traefik.localhost`: override the default domain +- `traefik.domain=traefik.localhost`: override the default domain +- `traefik.docker.network`: Set the docker network to use for connections to this container ## Marathon backend @@ -590,7 +636,16 @@ domain = "marathon.localhost" # Optional # Default: false # -# ExposedByDefault = true +# exposedByDefault = true + +# Convert Marathon groups to subdomains +# Default behavior: /foo/bar/myapp => foo-bar-myapp.{defaultDomain} +# with groupsAsSubDomains enabled: /foo/bar/myapp => myapp.bar.foo.{defaultDomain} +# +# Optional +# Default: false +# +# groupsAsSubDomains = true # Enable Marathon basic authentication # @@ -644,7 +699,7 @@ Træfɪk can be configured to use Kubernetes Ingress as a backend configuration: # and KUBERNETES_SERVICE_PORT_HTTPS as endpoint # Secure token will be found in /var/run/secrets/kubernetes.io/serviceaccount/token # and SSL CA cert in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt -# +# # Optional # # endpoint = "http://localhost:8080" @@ -741,6 +796,13 @@ domain = "consul.localhost" # Optional # prefix = "traefik" + +# Constraint on Consul catalog tags +# +# Optional +# +constraints = ["tag==api", "tag==he*ld"] +# Matching with containers having this tag: "traefik.tags=api,helloworld" ``` This backend will create routes matching on hostname based on the service name @@ -934,7 +996,7 @@ The Keys-Values structure should look (using `prefix = "/traefik"`): | `/traefik/frontends/frontend2/backend` | `backend1` | | `/traefik/frontends/frontend2/passHostHeader` | `true` | | `/traefik/frontends/frontend2/entrypoints` | `http,https` | -| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path:/test` | +| `/traefik/frontends/frontend2/routes/test_2/rule` | `PathPrefix:/test` | ## Atomic configuration changes @@ -973,4 +1035,3 @@ Once the `/traefik/alias` key is updated, the new `/traefik_configurations/2` co | `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` | Note that Træfɪk *will not watch for key changes in the `/traefik_configurations` prefix*. It will only watch for changes in the `/traefik` prefix. Further, if the `/traefik/alias` key is set, all other sibling keys with the `/traefik` prefix are ignored. - diff --git a/examples/compose-k8s.yaml b/examples/compose-k8s.yaml index e9abe96b4..626ea1d1c 100644 --- a/examples/compose-k8s.yaml +++ b/examples/compose-k8s.yaml @@ -1,8 +1,3 @@ -# etcd: -# image: gcr.io/google_containers/etcd:2.2.1 -# net: host -# command: ['/usr/local/bin/etcd', '--addr=127.0.0.1:4001', '--bind-addr=0.0.0.0:4001', '--data-dir=/var/etcd/data'] - kubelet: image: gcr.io/google_containers/hyperkube-amd64:v1.2.2 privileged: true diff --git a/examples/k8s.rc.yaml b/examples/k8s.rc.yaml index d7232b37d..43d3029d5 100644 --- a/examples/k8s.rc.yaml +++ b/examples/k8s.rc.yaml @@ -16,7 +16,7 @@ spec: spec: terminationGracePeriodSeconds: 60 containers: - - image: containous/traefik + - image: traefik name: traefik-ingress-lb imagePullPolicy: Always ports: diff --git a/examples/whoami-group.json b/examples/whoami-group.json new file mode 100644 index 000000000..9c21a8dae --- /dev/null +++ b/examples/whoami-group.json @@ -0,0 +1,40 @@ +{ + "id": "/foo", + "groups": [ + { + "id": "/foo/bar", + "apps": [ + { + "id": "whoami", + "cpus": 0.1, + "mem": 64.0, + "instances": 3, + "container": { + "type": "DOCKER", + "docker": { + "image": "emilevauge/whoami", + "network": "BRIDGE", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 0, + "protocol": "tcp" + } + ] + } + }, + "healthChecks": [ + { + "protocol": "HTTP", + "portIndex": 0, + "path": "/", + "gracePeriodSeconds": 5, + "intervalSeconds": 20, + "maxConsecutiveFailures": 3 + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/glide.lock b/glide.lock index 6c78fffae..05374f993 100644 --- a/glide.lock +++ b/glide.lock @@ -1,39 +1,40 @@ -hash: da7239dce8bda69f6e10b2f2bfae57dd4fd95b817055dca1379a72af42939b97 -updated: 2016-05-12T11:48:22.158455011+02:00 +hash: dc59755b72e71945a21135c5a37e4a5c11ae511ac7404d1440166ea0aed736c4 +updated: 2016-06-02T15:11:52.77657652+02:00 imports: -- name: github.com/alecthomas/template - version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 -- name: github.com/alecthomas/units - version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 - name: github.com/boltdb/bolt - version: 51f99c862475898df9773747d3accd05a7ca33c1 + version: dfb21201d9270c1082d5fb0f07f500311ff72f18 - name: github.com/BurntSushi/toml - version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f + version: f0aeabca5a127c4078abb8c8d64298b147264b55 - name: github.com/BurntSushi/ty version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 subpackages: - fun - name: github.com/cenkalti/backoff - version: 4dc77674aceaabba2c7e3da25d4c823edfb73f99 + version: a6030178a585d5972d4d33ce61f4a1fa40eaaed0 - name: github.com/codahale/hdrhistogram - version: 954f16e8b9ef0e5d5189456aa4c1202758e04f17 + version: 9208b142303c12d8899bae836fd524ac9338b4fd - name: github.com/codegangsta/cli version: bf4a526f48af7badd25d2cb02d587e1b01be3b50 - name: github.com/codegangsta/negroni - version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b + version: fb7b7c045dfb05dc81a5c3688c568550b5bd6e36 +- name: github.com/containous/flaeg + version: b98687da5c323650f4513fda6b6203fcbdec9313 - name: github.com/containous/oxy - version: 021f82bd8260ba15f5862a9fe62018437720dff5 + version: 183212964e13e7b8afe01a08b193d04300554a68 subpackages: - cbreaker - - forward - - memmetrics - - roundrobin - - utils - connlimit + - forward + - roundrobin - stream + - utils +- name: github.com/containous/staert + version: e2aa88e235a02dd52aa1d5d9de75f9d9139d1602 - name: github.com/coreos/etcd - version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637 + version: c400d05d0aa73e21e431c16145e558d624098018 subpackages: + - Godeps/_workspace/src/github.com/ugorji/go/codec + - Godeps/_workspace/src/golang.org/x/net/context - client - pkg/pathutil - pkg/types @@ -42,74 +43,39 @@ imports: subpackages: - spew - name: github.com/docker/distribution - version: 467fc068d88aa6610691b7f1a677271a3fac4aac + version: bb330cd684eb4afab9cc4f2453d7c8918099d7ee subpackages: - reference - digest - name: github.com/docker/docker version: 9837ec4da53f15f9120d53a6e1517491ba8b0261 subpackages: - - autogen - - api - - cliconfig - - daemon/network - - graph/tags - - image - - opts - - pkg/archive - - pkg/fileutils - - pkg/homedir - - pkg/httputils - - pkg/ioutils - - pkg/jsonmessage - - pkg/mflag - - pkg/nat - - pkg/parsers - - pkg/pools - - pkg/promise - - pkg/random - - pkg/stdcopy - - pkg/stringid - - pkg/symlink - - pkg/system - - pkg/tarsum - - pkg/term - - pkg/timeutils - - pkg/tlsconfig - - pkg/ulimit - - pkg/units - - pkg/urlutil - - pkg/useragent - - pkg/version - - registry - - runconfig - - utils - - volume + - namesgenerator - name: github.com/docker/engine-api version: 3d3d0b6c9d2651aac27f416a6da0224c1875b3eb subpackages: - client - types - - types/container - - types/filters - - types/strslice - types/events + - types/filters - client/transport - client/transport/cancellable + - types/container - types/network - types/reference - types/registry - types/time - types/versions - types/blkiodev + - types/strslice - name: github.com/docker/go-connections - version: 5b7154ba2efe13ff86ae8830a9e7cb120b080d6e + version: c7838b258fbfa3fe88eecfb2a0e08ea0dbd6a646 subpackages: - - nat - sockets - tlsconfig + - nat - name: github.com/docker/go-units - version: 5d2041e26a699eaca682e2ea41c8f891e1060444 + version: 09dda9d4b0d748c57c14048906d3d094a58ec0c9 - name: github.com/docker/libcompose version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873 - name: github.com/docker/libkv @@ -120,61 +86,39 @@ imports: - store/consul - store/etcd - store/zookeeper -- name: github.com/docker/libtrust - version: 9cbd2a1374f46905c68a4eb3694a130610adc62a - name: github.com/donovanhide/eventsource - version: d8a3071799b98cacd30b6da92f536050ccfe6da4 + version: fd1de70867126402be23c306e1ce32828455d85b - name: github.com/elazarl/go-bindata-assetfs - version: d5cac425555ca5cf00694df246e04f05e6a55150 -- name: github.com/flynn/go-shlex - version: 3f9db97f856818214da2e1057f8ad84803971cff + version: 57eb5e1fc594ad4b0b1dbea7b286d299e0cb43c2 - name: github.com/gambol99/go-marathon version: ade11d1dc2884ee1f387078fc28509559b6235d1 - name: github.com/go-check/check - version: 11d3bc7aa68e238947792f30573146a3231fc0f1 -- name: github.com/golang/glog - version: fca8c8854093a154ff1eb580aae10276ad6b1b5f + version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/google/go-querystring version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 subpackages: - query - name: github.com/gorilla/context - version: 215affda49addc4c8ef7e2534915df2c8c35c6cd -- name: github.com/gorilla/handlers - version: 40694b40f4a928c062f56849989d3e9cd0570e5f + version: aed02d124ae4a0e94fea4541c8effd05bf0c8296 - name: github.com/gorilla/mux - version: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a -- name: github.com/gorilla/websocket - version: 1f512fc3f05332ba7117626cdfb4e07474e58e60 + version: bd09be08ed4377796d312df0a45314e11b8f5dc1 - name: github.com/hashicorp/consul - version: de080672fee9e6104572eeea89eccdca135bb918 + version: ebf7ea1d759184c02a5bb5263a7c52d29838ffc3 subpackages: - api -- name: github.com/hashicorp/hcl - version: 9a905a34e6280ce905da1a32344b25e81011197a +- name: github.com/hashicorp/go-cleanhttp + version: 875fb671b3ddc66f8e2f0acc33829c8cb989a38d +- name: github.com/hashicorp/serf + version: e4ec8cc423bbe20d26584b96efbeb9102e16d05f subpackages: - - hcl/ast - - hcl/parser - - hcl/token - - json/parser - - hcl/scanner - - hcl/strconv - - json/scanner - - json/token -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 -- name: github.com/kr/pretty - version: add1dbc86daf0f983cd4a48ceb39deb95c729b67 -- name: github.com/kr/text - version: 7cafcd837844e784b526369c9bce262804aebc60 + - coordinate + - serf - name: github.com/libkermit/docker version: 3b5eb2973efff7af33cfb65141deaf4ed25c6d02 - name: github.com/libkermit/docker-check version: bb75a86b169c6c5d22c0ee98278124036f272d7b -- name: github.com/magiconair/properties - version: c265cfa48dda6474e208715ca93e987829f572f8 - name: github.com/mailgun/log - version: 44874009257d4d47ba9806f1b7f72a32a015e4d8 + version: 2f35a4607f1abf71f97f77f99b0de8493ef6f4ef - name: github.com/mailgun/manners version: fada45142db3f93097ca917da107aa3fad0ffcb5 - name: github.com/mailgun/multibuf @@ -184,60 +128,46 @@ imports: - name: github.com/mattn/go-shellwords version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24 - name: github.com/Microsoft/go-winio - version: 3b8b3c98b207f95fe0cd6c7c311a9ac497ba7c0f + version: 4f1a71750d95a5a8a46c40a67ffbed8129c2f138 - name: github.com/miekg/dns version: 48ab6605c66ac797e07f615101c3e9e10e932b66 -- name: github.com/mitchellh/mapstructure - version: d2dd0262208475919e1a362f675cfc0e7c10e905 - name: github.com/moul/http2curl - version: 1812aee76a1ce98d604a44200c6a23c689b17a89 + version: b1479103caacaa39319f75e7f57fc545287fca0d +- name: github.com/ogier/pflag + version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc - version: 2441732d6fcc0fb0a542671a4372e0c7bc99c19e + version: 6c485e6902bb9dd77b8234042b8f00e20ef87a18 subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest - version: a39a2f8d0463091df7344dbf586a9986e9f7184f + version: f17fef20c518e688f4edb3eb2af148462ecab3ef - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: - difflib +- name: github.com/ryanuber/go-glob + version: 572520ed46dbddaed19ea3d9541bdd0494163693 - name: github.com/samuel/go-zookeeper - version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b + version: 4b20de542e40ed2b89d65ae195fc20a330919b92 subpackages: - zk - name: github.com/Sirupsen/logrus - version: 418b41d23a1bf978c06faea5313ba194650ac088 -- name: github.com/spf13/cast - version: ee7b3e0353166ab1f3a605294ac8cd2b77953778 -- name: github.com/spf13/cobra - version: 0f866a6211e33cde2091d9290c08f6afd6c9ebbc - subpackages: - - cobra -- name: github.com/spf13/jwalterweatherman - version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 -- name: github.com/spf13/pflag - version: cb88ea77998c3f024757528e3305022ab50b43be -- name: github.com/spf13/viper - version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 + version: f3cfb454f4c209e6668c95216c4744b8fddb2356 - name: github.com/streamrail/concurrent-map - version: 1ce4642e5a162df67825d273a86b87e6cc8a076b + version: 65a174a3a4188c0b7099acbc6cfa0c53628d3287 - name: github.com/stretchr/objx version: cbeaeb16a013161a98496fad62933b1d21786672 - name: github.com/stretchr/testify - version: 6cb3b85ef5a0efef77caef88363ec4d4b5c0976d + version: 8d64eb7173c7753d6419fd4a9caf057398611364 subpackages: - mock - assert - name: github.com/thoas/stats - version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 -- name: github.com/ugorji/go - version: ea9cd21fa0bc41ee4bdd50ac7ed8cbc7ea2ed960 - subpackages: - - codec + version: 69e3c072eec2df2df41afe6214f62eb940e4cd80 - name: github.com/unrolled/render - version: 26b4e3aac686940fe29521545afad9966ddfc80c + version: 198ad4d8b8a4612176b804ca10555b222a086b40 - name: github.com/vdemeester/docker-events - version: ce5347b72aafad4e3bebd966f15e4183839d5172 + version: 20e6d2db238723e68197a9e3c6c34c99a9893a9c - name: github.com/vdemeester/shakers version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 - name: github.com/vulcand/oxy @@ -255,18 +185,16 @@ imports: - plugin/rewrite - plugin - router -- name: github.com/wendal/errors - version: f66c77a7882b399795a8987ebf87ef64a427417e - name: github.com/xenolf/lego - version: 948483535f53c34d144419869ecbed86251a30f6 + version: 30a7a8e8821de3532192d1240a45e53c6204f603 subpackages: - acme - name: golang.org/x/crypto - version: b76c864ef1dca1d8f271f917c290cddcce3d9e0d + version: 5bcd134fee4dd1475da17714aac19c0aa0142e2f subpackages: - ocsp - name: golang.org/x/net - version: d9558e5c97f85372afee28cf2b6059d7d3818919 + version: 6460565bec1e8891e29ff478184c71b9e443ac36 subpackages: - context - publicsuffix @@ -275,12 +203,11 @@ imports: version: eb2c74142fd19a79b3f237334c7384d5167b1b46 subpackages: - unix -- name: gopkg.in/alecthomas/kingpin.v2 - version: 639879d6110b1b0409410c7b737ef0bb18325038 + - windows - name: gopkg.in/fsnotify.v1 - version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 + version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8 - name: gopkg.in/mgo.v2 - version: 22287bab4379e1fbf6002fb4eb769888f3fb224c + version: b6e2fa371e64216a45e61072a96d4e3859f169da subpackages: - bson - name: gopkg.in/square/go-jose.v1 @@ -288,6 +215,4 @@ imports: subpackages: - cipher - json -- name: gopkg.in/yaml.v2 - version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 devImports: [] diff --git a/glide.yaml b/glide.yaml index 4c55bbb92..9a1e4385c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,189 +1,79 @@ -package: main +package: github.com/containous/traefik import: -- package: github.com/coreos/etcd - version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637 +- package: github.com/BurntSushi/toml +- package: github.com/BurntSushi/ty subpackages: - - client -- package: github.com/mailgun/log - version: 44874009257d4d47ba9806f1b7f72a32a015e4d8 + - fun +- package: github.com/Sirupsen/logrus +- package: github.com/cenkalti/backoff +- package: github.com/codegangsta/negroni +- package: github.com/containous/flaeg + version: b98687da5c323650f4513fda6b6203fcbdec9313 - package: github.com/containous/oxy - version: 021f82bd8260ba15f5862a9fe62018437720dff5 subpackages: - cbreaker + - connlimit - forward - - memmetrics - roundrobin + - stream - utils -- package: github.com/hashicorp/consul - version: de080672fee9e6104572eeea89eccdca135bb918 - subpackages: - - api -- package: github.com/samuel/go-zookeeper - version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b - subpackages: - - zk -- package: github.com/docker/libtrust - version: 9cbd2a1374f46905c68a4eb3694a130610adc62a -- package: github.com/go-check/check - version: 11d3bc7aa68e238947792f30573146a3231fc0f1 -- package: golang.org/x/net - version: d9558e5c97f85372afee28cf2b6059d7d3818919 - subpackages: - - context -- package: github.com/gorilla/handlers - version: 40694b40f4a928c062f56849989d3e9cd0570e5f -- package: github.com/docker/libkv - version: 7283ef27ed32fe267388510a91709b307bb9942c -- package: github.com/alecthomas/template - version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 -- package: github.com/vdemeester/shakers - version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 -- package: github.com/alecthomas/units - version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 -- package: github.com/gambol99/go-marathon - version: ade11d1dc2884ee1f387078fc28509559b6235d1 -- package: github.com/vulcand/predicate - version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 -- package: github.com/thoas/stats - version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 -- package: github.com/Sirupsen/logrus - version: 418b41d23a1bf978c06faea5313ba194650ac088 -- package: github.com/unrolled/render - version: 26b4e3aac686940fe29521545afad9966ddfc80c -- package: github.com/flynn/go-shlex - version: 3f9db97f856818214da2e1057f8ad84803971cff -- package: github.com/boltdb/bolt - version: 51f99c862475898df9773747d3accd05a7ca33c1 -- package: gopkg.in/mgo.v2 - version: 22287bab4379e1fbf6002fb4eb769888f3fb224c - subpackages: - - bson -- package: github.com/docker/docker - version: 9837ec4da53f15f9120d53a6e1517491ba8b0261 - subpackages: - - autogen - - api - - cliconfig - - daemon/network - - graph/tags - - image - - opts - - pkg/archive - - pkg/fileutils - - pkg/homedir - - pkg/httputils - - pkg/ioutils - - pkg/jsonmessage - - pkg/mflag - - pkg/nat - - pkg/parsers - - pkg/pools - - pkg/promise - - pkg/random - - pkg/stdcopy - - pkg/stringid - - pkg/symlink - - pkg/system - - pkg/tarsum - - pkg/term - - pkg/timeutils - - pkg/tlsconfig - - pkg/ulimit - - pkg/units - - pkg/urlutil - - pkg/useragent - - pkg/version - - registry - - runconfig - - utils - - volume -- package: github.com/mailgun/timetools - version: fd192d755b00c968d312d23f521eb0cdc6f66bd0 -- package: github.com/codegangsta/negroni - version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b -- package: gopkg.in/yaml.v2 - version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf -- package: github.com/opencontainers/runc - version: 2441732d6fcc0fb0a542671a4372e0c7bc99c19e - subpackages: - - libcontainer/user -- package: github.com/gorilla/mux - version: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a -- package: github.com/BurntSushi/ty - version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 -- package: github.com/elazarl/go-bindata-assetfs - version: d5cac425555ca5cf00694df246e04f05e6a55150 -- package: github.com/BurntSushi/toml - version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f -- package: gopkg.in/alecthomas/kingpin.v2 - version: 639879d6110b1b0409410c7b737ef0bb18325038 -- package: github.com/cenkalti/backoff - version: 4dc77674aceaabba2c7e3da25d4c823edfb73f99 -- package: gopkg.in/fsnotify.v1 - version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 -- package: github.com/mailgun/manners - version: fada45142db3f93097ca917da107aa3fad0ffcb5 -- package: github.com/gorilla/context - version: 215affda49addc4c8ef7e2534915df2c8c35c6cd -- package: github.com/codahale/hdrhistogram - version: 954f16e8b9ef0e5d5189456aa4c1202758e04f17 -- package: github.com/gorilla/websocket -- package: github.com/donovanhide/eventsource - version: d8a3071799b98cacd30b6da92f536050ccfe6da4 -- package: github.com/golang/glog - version: fca8c8854093a154ff1eb580aae10276ad6b1b5f -- package: github.com/spf13/cast - version: ee7b3e0353166ab1f3a605294ac8cd2b77953778 -- package: github.com/mitchellh/mapstructure -- package: github.com/spf13/jwalterweatherman -- package: github.com/spf13/pflag -- package: github.com/wendal/errors -- package: github.com/hashicorp/hcl -- package: github.com/kr/pretty -- package: github.com/magiconair/properties -- package: github.com/kr/text -- package: github.com/spf13/viper - version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 -- package: github.com/spf13/cobra - subpackages: - - cobra -- package: github.com/google/go-querystring - subpackages: - - query -- package: github.com/vulcand/vulcand - subpackages: - - plugin/rewrite -- package: github.com/stretchr/testify - subpackages: - - mock -- package: github.com/xenolf/lego -- package: github.com/libkermit/docker-check - version: bb75a86b169c6c5d22c0ee98278124036f272d7b -- package: github.com/libkermit/docker - version: 3b5eb2973efff7af33cfb65141deaf4ed25c6d02 -- package: github.com/docker/libcompose - version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873 -- package: github.com/docker/distribution - version: 467fc068d88aa6610691b7f1a677271a3fac4aac - subpackages: - - reference +- package: github.com/containous/staert + version: e2aa88e235a02dd52aa1d5d9de75f9d9139d1602 - package: github.com/docker/engine-api version: 3d3d0b6c9d2651aac27f416a6da0224c1875b3eb subpackages: - client - types - - types/container + - types/events - types/filters - - types/strslice -- package: github.com/vdemeester/docker-events - package: github.com/docker/go-connections subpackages: - - nat - sockets - tlsconfig -- package: github.com/docker/go-units -- package: github.com/mailgun/multibuf -- package: github.com/streamrail/concurrent-map +- package: github.com/docker/libkv + subpackages: + - store + - store/boltdb + - store/consul + - store/etcd + - store/zookeeper +- package: github.com/elazarl/go-bindata-assetfs +- package: github.com/gambol99/go-marathon + version: ade11d1dc2884ee1f387078fc28509559b6235d1 +- package: github.com/gorilla/mux +- package: github.com/hashicorp/consul + subpackages: + - api +- package: github.com/mailgun/manners - package: github.com/parnurzeal/gorequest +- package: github.com/streamrail/concurrent-map +- package: github.com/stretchr/testify + subpackages: + - mock +- package: github.com/thoas/stats +- package: github.com/unrolled/render +- package: github.com/vdemeester/docker-events +- package: github.com/vulcand/vulcand + subpackages: + - plugin/rewrite +- package: github.com/xenolf/lego + subpackages: + - acme +- package: golang.org/x/net + subpackages: + - context +- package: gopkg.in/fsnotify.v1 +- package: github.com/libkermit/docker-check + version: bb75a86b169c6c5d22c0ee98278124036f272d7b +- package: github.com/libkermit/docker + version: 3b5eb2973efff7af33cfb65141deaf4ed25c6d02 +- package: github.com/docker/docker + version: 9837ec4da53f15f9120d53a6e1517491ba8b0261 + subpackages: + - namesgenerator +- package: github.com/go-check/check +- package: github.com/docker/libcompose + version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873 - package: github.com/mattn/go-shellwords -- package: github.com/moul/http2curl +- package: github.com/vdemeester/shakers +- package: github.com/ryanuber/go-glob diff --git a/integration/basic_test.go b/integration/basic_test.go index 40f5ab997..1ef8521cc 100644 --- a/integration/basic_test.go +++ b/integration/basic_test.go @@ -5,7 +5,6 @@ import ( "os/exec" "time" - "fmt" "github.com/go-check/check" "bytes" @@ -15,34 +14,6 @@ import ( // SimpleSuite type SimpleSuite struct{ BaseSuite } -func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { - cmd := exec.Command(traefikBinary) - - var b bytes.Buffer - cmd.Stdout = &b - cmd.Stderr = &b - - cmd.Start() - time.Sleep(500 * time.Millisecond) - output := b.Bytes() - - c.Assert(string(output), checker.Contains, "No configuration file found") - cmd.Process.Kill() - - nonExistentFile := "non/existent/file.toml" - cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile) - - cmd.Stdout = &b - cmd.Stderr = &b - - cmd.Start() - time.Sleep(500 * time.Millisecond) - output = b.Bytes() - - c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading configuration file: open %s: no such file or directory", nonExistentFile)) - cmd.Process.Kill() -} - func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) { cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml") @@ -55,7 +26,7 @@ func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) { defer cmd.Process.Kill() output := b.Bytes() - c.Assert(string(output), checker.Contains, "While parsing config: Near line 0 (last key parsed ''): Bare keys cannot contain '{'") + c.Assert(string(output), checker.Contains, "Near line 0 (last key parsed ''): Bare keys cannot contain '{'") } func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) { @@ -86,3 +57,34 @@ func (s *SimpleSuite) TestWithWebConfig(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(resp.StatusCode, checker.Equals, 200) } + +func (s *SimpleSuite) TestDefaultEntryPoints(c *check.C) { + cmd := exec.Command(traefikBinary, "--debug") + + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + + cmd.Start() + time.Sleep(500 * time.Millisecond) + defer cmd.Process.Kill() + output := b.Bytes() + + c.Assert(string(output), checker.Contains, "\\\"DefaultEntryPoints\\\":[\\\"http\\\"]") +} + +func (s *SimpleSuite) TestPrintHelp(c *check.C) { + cmd := exec.Command(traefikBinary, "--help") + + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + + cmd.Start() + time.Sleep(500 * time.Millisecond) + defer cmd.Process.Kill() + output := b.Bytes() + + c.Assert(string(output), checker.Not(checker.Contains), "panic:") + c.Assert(string(output), checker.Contains, "Usage:") +} diff --git a/integration/constraint_test.go b/integration/constraint_test.go new file mode 100644 index 000000000..66ad8bbc0 --- /dev/null +++ b/integration/constraint_test.go @@ -0,0 +1,209 @@ +package main + +import ( + "net/http" + "os/exec" + "time" + + "github.com/go-check/check" + "github.com/hashicorp/consul/api" + + checker "github.com/vdemeester/shakers" +) + +// Constraint test suite +type ConstraintSuite struct { + BaseSuite + consulIP string + consulClient *api.Client +} + +func (s *ConstraintSuite) SetUpSuite(c *check.C) { + + s.createComposeProject(c, "constraints") + s.composeProject.Start(c) + + consul := s.composeProject.Container(c, "consul") + + s.consulIP = consul.NetworkSettings.IPAddress + config := api.DefaultConfig() + config.Address = s.consulIP + ":8500" + consulClient, err := api.NewClient(config) + if err != nil { + c.Fatalf("Error creating consul client") + } + s.consulClient = consulClient + + // Wait for consul to elect itself leader + time.Sleep(2000 * time.Millisecond) +} + +func (s *ConstraintSuite) registerService(name string, address string, port int, tags []string) error { + catalog := s.consulClient.Catalog() + _, err := catalog.Register( + &api.CatalogRegistration{ + Node: address, + Address: address, + Service: &api.AgentService{ + ID: name, + Service: name, + Address: address, + Port: port, + Tags: tags, + }, + }, + &api.WriteOptions{}, + ) + return err +} + +func (s *ConstraintSuite) deregisterService(name string, address string) error { + catalog := s.consulClient.Catalog() + _, err := catalog.Deregister( + &api.CatalogDeregistration{ + Node: address, + Address: address, + ServiceID: name, + }, + &api.WriteOptions{}, + ) + return err +} + +func (s *ConstraintSuite) TestMatchConstraintGlobal(c *check.C) { + cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml", "--constraints=tag==api") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + time.Sleep(5000 * time.Millisecond) + client := &http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + resp, err := client.Do(req) + + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 200) +} + +func (s *ConstraintSuite) TestDoesNotMatchConstraintGlobal(c *check.C) { + cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml", "--constraints=tag==api") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + time.Sleep(5000 * time.Millisecond) + client := &http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + resp, err := client.Do(req) + + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 404) +} + +func (s *ConstraintSuite) TestMatchConstraintProvider(c *check.C) { + cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml", "--consulCatalog.constraints=tag==api") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + time.Sleep(5000 * time.Millisecond) + client := &http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + resp, err := client.Do(req) + + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 200) +} + +func (s *ConstraintSuite) TestDoesNotMatchConstraintProvider(c *check.C) { + cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml", "--consulCatalog.constraints=tag==api") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + time.Sleep(5000 * time.Millisecond) + client := &http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + resp, err := client.Do(req) + + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 404) +} + +func (s *ConstraintSuite) TestMatchMultipleConstraint(c *check.C) { + cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml", "--consulCatalog.constraints=tag==api", "--constraints=tag!=us-*") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api", "traefik.tags=eu-1"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + time.Sleep(5000 * time.Millisecond) + client := &http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + resp, err := client.Do(req) + + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 200) +} + +func (s *ConstraintSuite) TestDoesNotMatchMultipleConstraint(c *check.C) { + cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml", "--consulCatalog.constraints=tag==api", "--constraints=tag!=us-*") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api", "traefik.tags=us-1"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + time.Sleep(5000 * time.Millisecond) + client := &http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + resp, err := client.Do(req) + + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, 404) +} diff --git a/integration/integration_test.go b/integration/integration_test.go index c31832a19..9d7bff2bb 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -31,6 +31,7 @@ func init() { check.Suite(&ConsulCatalogSuite{}) check.Suite(&EtcdSuite{}) check.Suite(&MarathonSuite{}) + check.Suite(&ConstraintSuite{}) } var traefikBinary = "../dist/traefik" diff --git a/integration/resources/compose/constraints.yml b/integration/resources/compose/constraints.yml new file mode 100644 index 000000000..9a2688904 --- /dev/null +++ b/integration/resources/compose/constraints.yml @@ -0,0 +1,17 @@ +consul: + image: progrium/consul + command: -server -bootstrap -log-level debug -ui-dir /ui + ports: + - "8400:8400" + - "8500:8500" + - "8600:53/udp" + expose: + - "8300" + - "8301" + - "8301/udp" + - "8302" + - "8302/udp" +nginx: + image: nginx + ports: + - "8881:80" diff --git a/provider/boltdb.go b/provider/boltdb.go index 0f941627e..574956ace 100644 --- a/provider/boltdb.go +++ b/provider/boltdb.go @@ -9,13 +9,13 @@ import ( // BoltDb holds configurations of the BoltDb provider. type BoltDb struct { - Kv `mapstructure:",squash"` + Kv } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { provider.storeType = store.BOLTDB boltdb.Register() - return provider.provide(configurationChan, pool) + return provider.provide(configurationChan, pool, constraints) } diff --git a/provider/consul.go b/provider/consul.go index a2df6852b..f936e79f3 100644 --- a/provider/consul.go +++ b/provider/consul.go @@ -9,13 +9,13 @@ import ( // Consul holds configurations of the Consul provider. type Consul struct { - Kv `mapstructure:",squash"` + Kv } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { provider.storeType = store.CONSUL consul.Register() - return provider.provide(configurationChan, pool) + return provider.provide(configurationChan, pool, constraints) } diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 90606e9d4..cce6db185 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -7,6 +7,7 @@ import ( "text/template" "time" + "github.com/BurntSushi/ty/fun" log "github.com/Sirupsen/logrus" "github.com/cenkalti/backoff" "github.com/containous/traefik/safe" @@ -23,11 +24,11 @@ const ( // ConsulCatalog holds configurations of the Consul catalog provider. type ConsulCatalog struct { - BaseProvider `mapstructure:",squash"` - Endpoint string - Domain string - client *api.Client - Prefix string + BaseProvider + Endpoint string `description:"Consul server endpoint"` + Domain string `description:"Default domain used"` + client *api.Client + Prefix string } type serviceUpdate struct { @@ -88,23 +89,29 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro return catalogUpdate{}, err } - set := map[string]bool{} - tags := []string{} - for _, node := range data { - for _, tag := range node.Service.Tags { - if _, ok := set[tag]; ok == false { - set[tag] = true - tags = append(tags, tag) - } + nodes := fun.Filter(func(node *api.ServiceEntry) bool { + constraintTags := provider.getContraintTags(node.Service.Tags) + ok, failingConstraint := provider.MatchConstraints(constraintTags) + if ok == false && failingConstraint != nil { + log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String()) } - } + return ok + }, data).([]*api.ServiceEntry) + + //Merge tags of nodes matching constraints, in a single slice. + tags := fun.Foldl(func(node *api.ServiceEntry, set []string) []string { + return fun.Keys(fun.Union( + fun.Set(set), + fun.Set(node.Service.Tags), + ).(map[string]bool)).([]string) + }, []string{}, nodes).([]string) return catalogUpdate{ Service: &serviceUpdate{ ServiceName: service, Attributes: tags, }, - Nodes: data, + Nodes: nodes, }, nil } @@ -157,6 +164,19 @@ func (provider *ConsulCatalog) getAttribute(name string, tags []string, defaultV return defaultValue } +func (provider *ConsulCatalog) getContraintTags(tags []string) []string { + var list []string + + for _, tag := range tags { + if strings.Index(strings.ToLower(tag), DefaultConsulCatalogTagPrefix+".tags=") == 0 { + splitedTags := strings.Split(tag[len(DefaultConsulCatalogTagPrefix+".tags="):], ",") + list = append(list, splitedTags...) + } + } + + return list +} + func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration { var FuncMap = template.FuncMap{ "getBackend": provider.getBackend, @@ -212,7 +232,10 @@ func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpd if err != nil { return nil, err } - nodes = append(nodes, healthy) + // healthy.Nodes can be empty if constraints do not match, without throwing error + if healthy.Service != nil && len(healthy.Nodes) > 0 { + nodes = append(nodes, healthy) + } } } return nodes, nil @@ -248,7 +271,7 @@ func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessag // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { config := api.DefaultConfig() config.Address = provider.Endpoint client, err := api.NewClient(config) @@ -256,6 +279,7 @@ func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMess return err } provider.client = client + provider.Constraints = append(provider.Constraints, constraints...) pool.Go(func(stop chan bool) { notify := func(err error, time time.Duration) { diff --git a/provider/docker.go b/provider/docker.go index 7169f20e1..657eabfda 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -29,18 +29,18 @@ const DockerAPIVersion string = "1.21" // Docker holds configurations of the Docker provider. type Docker struct { - BaseProvider `mapstructure:",squash"` - Endpoint string - Domain string - TLS *DockerTLS + BaseProvider + Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"` + Domain string `description:"Default domain used"` + TLS *DockerTLS `description:"Enable Docker TLS support"` } // DockerTLS holds TLS specific configurations type DockerTLS struct { - CA string - Cert string - Key string - InsecureSkipVerify bool + CA string `description:"TLS CA"` + Cert string `description:"TLS cert"` + Key string `description:"TLS key"` + InsecureSkipVerify bool `description:"TLS insecure skip verify"` } func (provider *Docker) createClient() (client.APIClient, error) { @@ -79,7 +79,8 @@ func (provider *Docker) createClient() (client.APIClient, error) { // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { + provider.Constraints = append(provider.Constraints, constraints...) // TODO register this routine in pool, and watch for stop channel safe.Go(func() { operation := func() error { @@ -160,6 +161,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.ContainerJSON) *types.Configuration { var DockerFuncMap = template.FuncMap{ "getBackend": provider.getBackend, + "getIPAddress": provider.getIPAddress, "getPort": provider.getPort, "getWeight": provider.getWeight, "getDomain": provider.getDomain, @@ -196,11 +198,11 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.Conta } func containerFilter(container dockertypes.ContainerJSON) bool { - if len(container.NetworkSettings.Ports) == 0 { - log.Debugf("Filtering container without port %s", container.Name) + _, err := strconv.Atoi(container.Config.Labels["traefik.port"]) + if len(container.NetworkSettings.Ports) == 0 && err != nil { + log.Debugf("Filtering container without port and no traefik.port label %s", container.Name) return false } - _, err := strconv.Atoi(container.Config.Labels["traefik.port"]) if len(container.NetworkSettings.Ports) > 1 && err != nil { log.Debugf("Filtering container with more than 1 port and no traefik.port label %s", container.Name) return false @@ -234,7 +236,7 @@ func (provider *Docker) getFrontendRule(container dockertypes.ContainerJSON) str if label, err := getLabel(container, "traefik.frontend.rule"); err == nil { return label } - return "Host:" + getEscapedName(container.Name) + "." + provider.Domain + return "Host:" + provider.getSubDomain(container.Name) + "." + provider.Domain } func (provider *Docker) getBackend(container dockertypes.ContainerJSON) string { @@ -244,6 +246,22 @@ func (provider *Docker) getBackend(container dockertypes.ContainerJSON) string { return normalize(container.Name) } +func (provider *Docker) getIPAddress(container dockertypes.ContainerJSON) string { + if label, err := getLabel(container, "traefik.docker.network"); err == nil && label != "" { + networks := container.NetworkSettings.Networks + if networks != nil { + network := networks[label] + if network != nil { + return network.IPAddress + } + } + } + for _, network := range container.NetworkSettings.Networks { + return network.IPAddress + } + return "" +} + func (provider *Docker) getPort(container dockertypes.ContainerJSON) string { if label, err := getLabel(container, "traefik.port"); err == nil { return label @@ -331,3 +349,8 @@ func listContainers(dockerClient client.APIClient) ([]dockertypes.ContainerJSON, } return containersInspected, nil } + +// Escape beginning slash "/", convert all others to dash "-" +func (provider *Docker) getSubDomain(name string) string { + return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) +} diff --git a/provider/docker_test.go b/provider/docker_test.go index 8cf5d2771..3605d280a 100644 --- a/provider/docker_test.go +++ b/provider/docker_test.go @@ -203,6 +203,82 @@ func TestDockerGetBackend(t *testing.T) { } } +func TestDockerGetIPAddress(t *testing.T) { // TODO + provider := &Docker{} + + containers := []struct { + container docker.ContainerJSON + expected string + }{ + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "bar", + }, + Config: &container.Config{}, + NetworkSettings: &docker.NetworkSettings{ + Networks: map[string]*network.EndpointSettings{ + "testnet": { + IPAddress: "10.11.12.13", + }, + }, + }, + }, + expected: "10.11.12.13", + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "bar", + }, + Config: &container.Config{ + Labels: map[string]string{ + "traefik.docker.network": "testnet", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + Networks: map[string]*network.EndpointSettings{ + "nottestnet": { + IPAddress: "10.11.12.13", + }, + }, + }, + }, + expected: "10.11.12.13", + }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "bar", + }, + Config: &container.Config{ + Labels: map[string]string{ + "traefik.docker.network": "testnet2", + }, + }, + NetworkSettings: &docker.NetworkSettings{ + Networks: map[string]*network.EndpointSettings{ + "testnet1": { + IPAddress: "10.11.12.13", + }, + "testnet2": { + IPAddress: "10.11.12.14", + }, + }, + }, + }, + expected: "10.11.12.14", + }, + } + + for _, e := range containers { + actual := provider.getIPAddress(e.container) + if actual != e.expected { + t.Fatalf("expected %q, got %q", e.expected, actual) + } + } +} + func TestDockerGetPort(t *testing.T) { provider := &Docker{} @@ -250,6 +326,20 @@ func TestDockerGetPort(t *testing.T) { // }, // expected: "80", // }, + { + container: docker.ContainerJSON{ + ContainerJSONBase: &docker.ContainerJSONBase{ + Name: "test", + }, + Config: &container.Config{ + Labels: map[string]string{ + "traefik.port": "8080", + }, + }, + NetworkSettings: &docker.NetworkSettings{}, + }, + expected: "8080", + }, { container: docker.ContainerJSON{ ContainerJSONBase: &docker.ContainerJSONBase{ diff --git a/provider/etcd.go b/provider/etcd.go index 3d0b9e428..934e0f245 100644 --- a/provider/etcd.go +++ b/provider/etcd.go @@ -9,13 +9,13 @@ import ( // Etcd holds configurations of the Etcd provider. type Etcd struct { - Kv `mapstructure:",squash"` + Kv } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { provider.storeType = store.ETCD etcd.Register() - return provider.provide(configurationChan, pool) + return provider.provide(configurationChan, pool, constraints) } diff --git a/provider/file.go b/provider/file.go index 6b943b199..07bcbd02f 100644 --- a/provider/file.go +++ b/provider/file.go @@ -14,12 +14,12 @@ import ( // File holds configurations of the File provider. type File struct { - BaseProvider `mapstructure:",squash"` + BaseProvider } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *File) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *File) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ []types.Constraint) error { watcher, err := fsnotify.NewWatcher() if err != nil { log.Error("Error creating file watcher", err) diff --git a/provider/k8s/client.go b/provider/k8s/client.go index 3122c9f44..0b2195615 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "encoding/json" "fmt" - "github.com/containous/traefik/safe" "github.com/parnurzeal/gorequest" "net/http" "net/url" @@ -17,12 +16,14 @@ const ( APIEndpoint = "/api/v1" extentionsEndpoint = "/apis/extensions/v1beta1" defaultIngress = "/ingresses" + namespaces = "/namespaces/" ) // Client is a client for the Kubernetes master. type Client interface { GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) - GetServices(predicate func(Service) bool) ([]Service, error) + GetService(name, namespace string) (Service, error) + GetEndpoints(name, namespace string) (Endpoints, error) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) } @@ -77,26 +78,20 @@ func (c *clientImpl) WatchIngresses(stopCh <-chan bool) (chan interface{}, chan return c.watch(getURL, stopCh) } -// GetServices returns all services in the cluster -func (c *clientImpl) GetServices(predicate func(Service) bool) ([]Service, error) { - getURL := c.endpointURL + APIEndpoint + "/services" +// GetService returns the named service from the named namespace +func (c *clientImpl) GetService(name, namespace string) (Service, error) { + getURL := c.endpointURL + APIEndpoint + namespaces + namespace + "/services/" + name body, err := c.do(c.request(getURL)) if err != nil { - return nil, fmt.Errorf("failed to create services request: GET %q : %v", getURL, err) + return Service{}, fmt.Errorf("failed to create services request: GET %q : %v", getURL, err) } - var serviceList ServiceList - if err := json.Unmarshal(body, &serviceList); err != nil { - return nil, fmt.Errorf("failed to decode list of services resources: %v", err) + var service Service + if err := json.Unmarshal(body, &service); err != nil { + return Service{}, fmt.Errorf("failed to decode service resource: %v", err) } - services := serviceList.Items[:0] - for _, service := range serviceList.Items { - if predicate(service) { - services = append(services, service) - } - } - return services, nil + return service, nil } // WatchServices returns all services in the cluster @@ -105,28 +100,33 @@ func (c *clientImpl) WatchServices(stopCh <-chan bool) (chan interface{}, chan e return c.watch(getURL, stopCh) } -// WatchEvents returns events in the cluster -func (c *clientImpl) WatchEvents(stopCh <-chan bool) (chan interface{}, chan error, error) { - getURL := c.endpointURL + APIEndpoint + "/events" - return c.watch(getURL, stopCh) +// GetEndpoints returns the named Endpoints +// Endpoints have the same name as the coresponding service +func (c *clientImpl) GetEndpoints(name, namespace string) (Endpoints, error) { + getURL := c.endpointURL + APIEndpoint + namespaces + namespace + "/endpoints/" + name + + body, err := c.do(c.request(getURL)) + if err != nil { + return Endpoints{}, fmt.Errorf("failed to create endpoints request: GET %q : %v", getURL, err) + } + + var endpoints Endpoints + if err := json.Unmarshal(body, &endpoints); err != nil { + return Endpoints{}, fmt.Errorf("failed to decode endpoints resources: %v", err) + } + return endpoints, nil } -// WatchPods returns pods in the cluster -func (c *clientImpl) WatchPods(stopCh <-chan bool) (chan interface{}, chan error, error) { - getURL := c.endpointURL + APIEndpoint + "/pods" - return c.watch(getURL, stopCh) -} - -// WatchReplicationControllers returns ReplicationControllers in the cluster -func (c *clientImpl) WatchReplicationControllers(stopCh <-chan bool) (chan interface{}, chan error, error) { - getURL := c.endpointURL + APIEndpoint + "/replicationcontrollers" +// WatchEndpoints returns endpoints in the cluster +func (c *clientImpl) WatchEndpoints(stopCh <-chan bool) (chan interface{}, chan error, error) { + getURL := c.endpointURL + APIEndpoint + "/endpoints" return c.watch(getURL, stopCh) } // WatchAll returns events in the cluster func (c *clientImpl) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) { - watchCh := make(chan interface{}) - errCh := make(chan error) + watchCh := make(chan interface{}, 10) + errCh := make(chan error, 10) stopIngresses := make(chan bool) chanIngresses, chanIngressesErr, err := c.WatchIngresses(stopIngresses) @@ -138,13 +138,8 @@ func (c *clientImpl) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, if err != nil { return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) } - stopPods := make(chan bool) - chanPods, chanPodsErr, err := c.WatchPods(stopPods) - if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) - } - stopReplicationControllers := make(chan bool) - chanReplicationControllers, chanReplicationControllersErr, err := c.WatchReplicationControllers(stopReplicationControllers) + stopEndpoints := make(chan bool) + chanEndpoints, chanEndpointsErr, err := c.WatchEndpoints(stopEndpoints) if err != nil { return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) } @@ -153,32 +148,26 @@ func (c *clientImpl) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, defer close(errCh) defer close(stopIngresses) defer close(stopServices) - defer close(stopPods) - defer close(stopReplicationControllers) + defer close(stopEndpoints) for { select { case <-stopCh: stopIngresses <- true stopServices <- true - stopPods <- true - stopReplicationControllers <- true - break + stopEndpoints <- true + return case err := <-chanIngressesErr: errCh <- err case err := <-chanServicesErr: errCh <- err - case err := <-chanPodsErr: - errCh <- err - case err := <-chanReplicationControllersErr: + case err := <-chanEndpointsErr: errCh <- err case event := <-chanIngresses: watchCh <- event case event := <-chanServices: watchCh <- event - case event := <-chanPods: - watchCh <- event - case event := <-chanReplicationControllers: + case event := <-chanEndpoints: watchCh <- event } } @@ -192,6 +181,7 @@ func (c *clientImpl) do(request *gorequest.SuperAgent) ([]byte, error) { if errs != nil { return nil, fmt.Errorf("failed to create request: GET %q : %v", request.Url, errs) } + defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, request.Url, string(body)) } @@ -201,6 +191,12 @@ func (c *clientImpl) do(request *gorequest.SuperAgent) ([]byte, error) { func (c *clientImpl) request(url string) *gorequest.SuperAgent { // Make request to Kubernetes API request := gorequest.New().Get(url) + request.Transport.DisableKeepAlives = true + + if strings.HasPrefix(url, "http://") { + return request + } + if len(c.token) > 0 { request.Header["Authorization"] = "Bearer " + c.token pool := x509.NewCertPool() @@ -217,8 +213,8 @@ type GenericObject struct { } func (c *clientImpl) watch(url string, stopCh <-chan bool) (chan interface{}, chan error, error) { - watchCh := make(chan interface{}) - errCh := make(chan error) + watchCh := make(chan interface{}, 10) + errCh := make(chan error, 10) // get version body, err := c.do(c.request(url)) @@ -240,34 +236,38 @@ func (c *clientImpl) watch(url string, stopCh <-chan bool) (chan interface{}, ch return watchCh, errCh, fmt.Errorf("failed to make watch request: GET %q : %v", url, err) } request.Client.Transport = request.Transport + res, err := request.Client.Do(req) if err != nil { return watchCh, errCh, fmt.Errorf("failed to do watch request: GET %q: %v", url, err) } - shouldStop := safe.New(false) - - go func() { - select { - case <-stopCh: - shouldStop.Set(true) - res.Body.Close() - return - } - }() - go func() { + finishCh := make(chan bool) + defer close(finishCh) defer close(watchCh) defer close(errCh) - for { - var eventList interface{} - if err := json.NewDecoder(res.Body).Decode(&eventList); err != nil { - if !shouldStop.Get().(bool) { - errCh <- fmt.Errorf("failed to decode watch event: %v", err) + go func() { + defer res.Body.Close() + for { + var eventList interface{} + if err := json.NewDecoder(res.Body).Decode(&eventList); err != nil { + if !strings.Contains(err.Error(), "net/http: request canceled") { + errCh <- fmt.Errorf("failed to decode watch event: GET %q : %v", url, err) + } + finishCh <- true + return } - return + watchCh <- eventList } - watchCh <- eventList + }() + select { + case <-stopCh: + go func() { + request.Transport.CancelRequest(req) + }() + <-finishCh + return } }() return watchCh, errCh, nil diff --git a/provider/k8s/endpoints.go b/provider/k8s/endpoints.go new file mode 100644 index 000000000..123ffe36c --- /dev/null +++ b/provider/k8s/endpoints.go @@ -0,0 +1,84 @@ +package k8s + +// Endpoints is a collection of endpoints that implement the actual service. Example: +// Name: "mysvc", +// Subsets: [ +// { +// Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}], +// Ports: [{"name": "a", "port": 8675}, {"name": "b", "port": 309}] +// }, +// { +// Addresses: [{"ip": "10.10.3.3"}], +// Ports: [{"name": "a", "port": 93}, {"name": "b", "port": 76}] +// }, +// ] +type Endpoints struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // The set of all endpoints is the union of all subsets. + Subsets []EndpointSubset +} + +// EndpointSubset is a group of addresses with a common set of ports. The +// expanded set of endpoints is the Cartesian product of Addresses x Ports. +// For example, given: +// { +// Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}], +// Ports: [{"name": "a", "port": 8675}, {"name": "b", "port": 309}] +// } +// The resulting set of endpoints can be viewed as: +// a: [ 10.10.1.1:8675, 10.10.2.2:8675 ], +// b: [ 10.10.1.1:309, 10.10.2.2:309 ] +type EndpointSubset struct { + Addresses []EndpointAddress + NotReadyAddresses []EndpointAddress + Ports []EndpointPort +} + +// EndpointAddress is a tuple that describes single IP address. +type EndpointAddress struct { + // The IP of this endpoint. + // IPv6 is also accepted but not fully supported on all platforms. Also, certain + // kubernetes components, like kube-proxy, are not IPv6 ready. + // TODO: This should allow hostname or IP, see #4447. + IP string + // Optional: Hostname of this endpoint + // Meant to be used by DNS servers etc. + Hostname string `json:"hostname,omitempty"` + // Optional: The kubernetes object related to the entry point. + TargetRef *ObjectReference +} + +// EndpointPort is a tuple that describes a single port. +type EndpointPort struct { + // The name of this port (corresponds to ServicePort.Name). Optional + // if only one port is defined. Must be a DNS_LABEL. + Name string + + // The port number. + Port int32 + + // The IP protocol for this port. + Protocol Protocol +} + +// ObjectReference contains enough information to let you inspect or modify the referred object. +type ObjectReference struct { + Kind string `json:"kind,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + UID UID `json:"uid,omitempty"` + APIVersion string `json:"apiVersion,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty"` + + // Optional. If referring to a piece of an object instead of an entire object, this string + // should contain information to identify the sub-object. For example, if the object + // reference is to a container within a pod, this would take on a value like: + // "spec.containers{name}" (where "name" refers to the name of the container that triggered + // the event) or if no container name is specified "spec.containers[2]" (container with + // index 2 in this pod). This syntax is chosen only to have some well-defined way of + // referencing a part of an object. + // TODO: this design is not final and this field is subject to change in the future. + FieldPath string `json:"fieldPath,omitempty"` +} diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 4af905084..e46b165d4 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -1,6 +1,7 @@ package provider import ( + "fmt" log "github.com/Sirupsen/logrus" "github.com/cenkalti/backoff" "github.com/containous/traefik/provider/k8s" @@ -20,12 +21,38 @@ const ( serviceAccountCACert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" ) +// Namespaces holds kubernetes namespaces +type Namespaces []string + +//Set adds strings elem into the the parser +//it splits str on , and ; +func (ns *Namespaces) Set(str string) error { + fargs := func(c rune) bool { + return c == ',' || c == ';' + } + // get function + slice := strings.FieldsFunc(str, fargs) + *ns = append(*ns, slice...) + return nil +} + +//Get []string +func (ns *Namespaces) Get() interface{} { return Namespaces(*ns) } + +//String return slice in a string +func (ns *Namespaces) String() string { return fmt.Sprintf("%v", *ns) } + +//SetValue sets []string into the parser +func (ns *Namespaces) SetValue(val interface{}) { + *ns = Namespaces(val.(Namespaces)) +} + // Kubernetes holds configurations of the Kubernetes provider. type Kubernetes struct { - BaseProvider `mapstructure:",squash"` - Endpoint string - disablePassHostHeaders bool - Namespaces []string + BaseProvider + Endpoint string `description:"Kubernetes server endpoint"` + DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers"` + Namespaces Namespaces `description:"Kubernetes namespaces"` } func (provider *Kubernetes) createClient() (k8s.Client, error) { @@ -54,27 +81,29 @@ func (provider *Kubernetes) createClient() (k8s.Client, error) { // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { k8sClient, err := provider.createClient() if err != nil { return err } backOff := backoff.NewExponentialBackOff() + provider.Constraints = append(provider.Constraints, constraints...) pool.Go(func(stop chan bool) { - stopWatch := make(chan bool) - defer close(stopWatch) operation := func() error { - select { - case <-stop: - return nil - default: - } for { + stopWatch := make(chan bool, 5) + defer close(stopWatch) eventsChan, errEventsChan, err := k8sClient.WatchAll(stopWatch) if err != nil { log.Errorf("Error watching kubernetes events: %v", err) - return err + timer := time.NewTimer(1 * time.Second) + select { + case <-timer.C: + return err + case <-stop: + return nil + } } Watch: for { @@ -82,14 +111,15 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage case <-stop: stopWatch <- true return nil - case err := <-errEventsChan: - if strings.Contains(err.Error(), io.EOF.Error()) { + case err, ok := <-errEventsChan: + stopWatch <- true + if ok && strings.Contains(err.Error(), io.EOF.Error()) { // edge case, kubernetes long-polling disconnection break Watch } return err case event := <-eventsChan: - log.Debugf("Received event from kubenetes %+v", event) + log.Debugf("Received event from kubernetes %+v", event) templateObjects, err := provider.loadIngresses(k8sClient) if err != nil { return err @@ -160,9 +190,11 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur Routes: make(map[string]types.Route), } } - if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists { - templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{ - Rule: "Host:" + r.Host, + if len(r.Host) > 0 { + if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists { + templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{ + Rule: "Host:" + r.Host, + } } } if len(pa.Path) > 0 { @@ -186,31 +218,42 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur Rule: ruleType + ":" + pa.Path, } } - services, err := k8sClient.GetServices(func(service k8s.Service) bool { - return service.ObjectMeta.Namespace == i.ObjectMeta.Namespace && service.Name == pa.Backend.ServiceName - }) + service, err := k8sClient.GetService(pa.Backend.ServiceName, i.ObjectMeta.Namespace) if err != nil { - log.Errorf("Error retrieving services: %v", err) + log.Warnf("Error retrieving services: %v", err) + delete(templateObjects.Frontends, r.Host+pa.Path) + log.Warnf("Error retrieving services %s", pa.Backend.ServiceName) continue } - if len(services) == 0 { - // no backends found, delete frontend... - delete(templateObjects.Frontends, r.Host+pa.Path) - log.Errorf("Error retrieving services %s", pa.Backend.ServiceName) - } - for _, service := range services { - protocol := "http" - for _, port := range service.Spec.Ports { - if equalPorts(port, pa.Backend.ServicePort) { - if port.Port == 443 { - protocol = "https" - } + protocol := "http" + for _, port := range service.Spec.Ports { + if equalPorts(port, pa.Backend.ServicePort) { + if port.Port == 443 { + protocol = "https" + } + endpoints, err := k8sClient.GetEndpoints(service.ObjectMeta.Name, service.ObjectMeta.Namespace) + if err != nil { + log.Errorf("Error retrieving endpoints: %v", err) + continue + } + if len(endpoints.Subsets) == 0 { + log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name) templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(port.Port), Weight: 1, } - break + } else { + for _, subset := range endpoints.Subsets { + for _, address := range subset.Addresses { + url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports)) + templateObjects.Backends[r.Host+pa.Path].Servers[url] = types.Server{ + URL: url, + Weight: 1, + } + } + } } + break } } } @@ -219,6 +262,20 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur return &templateObjects, nil } +func endpointPortNumber(servicePort k8s.ServicePort, endpointPorts []k8s.EndpointPort) int { + if len(endpointPorts) > 0 { + //name is optional if there is only one port + port := endpointPorts[0] + for _, endpointPort := range endpointPorts { + if servicePort.Name == endpointPort.Name { + port = endpointPort + } + } + return int(port.Port) + } + return servicePort.Port +} + func equalPorts(servicePort k8s.ServicePort, ingressPort k8s.IntOrString) bool { if servicePort.Port == ingressPort.IntValue() { return true @@ -230,7 +287,7 @@ func equalPorts(servicePort k8s.ServicePort, ingressPort k8s.IntOrString) bool { } func (provider *Kubernetes) getPassHostHeader() bool { - if provider.disablePassHostHeaders { + if provider.DisablePassHostHeaders { return false } return true diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 0b9b98d62..25e48b8d7 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -10,6 +10,9 @@ import ( func TestLoadIngresses(t *testing.T) { ingresses := []k8s.Ingress{{ + ObjectMeta: k8s.ObjectMeta{ + Namespace: "testing", + }, Spec: k8s.IngressSpec{ Rules: []k8s.IngressRule{ { @@ -21,7 +24,7 @@ func TestLoadIngresses(t *testing.T) { Path: "/bar", Backend: k8s.IngressBackend{ ServiceName: "service1", - ServicePort: k8s.FromString("http"), + ServicePort: k8s.FromInt(80), }, }, }, @@ -36,7 +39,7 @@ func TestLoadIngresses(t *testing.T) { { Backend: k8s.IngressBackend{ ServiceName: "service3", - ServicePort: k8s.FromInt(443), + ServicePort: k8s.FromString("https"), }, }, { @@ -55,23 +58,24 @@ func TestLoadIngresses(t *testing.T) { services := []k8s.Service{ { ObjectMeta: k8s.ObjectMeta{ - Name: "service1", - UID: "1", + Name: "service1", + UID: "1", + Namespace: "testing", }, Spec: k8s.ServiceSpec{ ClusterIP: "10.0.0.1", Ports: []k8s.ServicePort{ { - Name: "http", - Port: 801, + Port: 80, }, }, }, }, { ObjectMeta: k8s.ObjectMeta{ - Name: "service2", - UID: "2", + Name: "service2", + UID: "2", + Namespace: "testing", }, Spec: k8s.ServiceSpec{ ClusterIP: "10.0.0.2", @@ -84,24 +88,108 @@ func TestLoadIngresses(t *testing.T) { }, { ObjectMeta: k8s.ObjectMeta{ - Name: "service3", - UID: "3", + Name: "service3", + UID: "3", + Namespace: "testing", }, Spec: k8s.ServiceSpec{ ClusterIP: "10.0.0.3", Ports: []k8s.ServicePort{ { Name: "http", + Port: 80, + }, + { + Name: "https", Port: 443, }, }, }, }, } + endpoints := []k8s.Endpoints{ + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service1", + UID: "1", + Namespace: "testing", + }, + Subsets: []k8s.EndpointSubset{ + { + Addresses: []k8s.EndpointAddress{ + { + IP: "10.10.0.1", + }, + }, + Ports: []k8s.EndpointPort{ + { + Port: 8080, + }, + }, + }, + { + Addresses: []k8s.EndpointAddress{ + { + IP: "10.21.0.1", + }, + }, + Ports: []k8s.EndpointPort{ + { + Port: 8080, + }, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service3", + UID: "3", + Namespace: "testing", + }, + Subsets: []k8s.EndpointSubset{ + { + Addresses: []k8s.EndpointAddress{ + { + IP: "10.15.0.1", + }, + }, + Ports: []k8s.EndpointPort{ + { + Name: "http", + Port: 8080, + }, + { + Name: "https", + Port: 8443, + }, + }, + }, + { + Addresses: []k8s.EndpointAddress{ + { + IP: "10.15.0.2", + }, + }, + Ports: []k8s.EndpointPort{ + { + Name: "http", + Port: 9080, + }, + { + Name: "https", + Port: 9443, + }, + }, + }, + }, + }, + } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, + endpoints: endpoints, watchChan: watchChan, } provider := Kubernetes{} @@ -114,8 +202,12 @@ func TestLoadIngresses(t *testing.T) { Backends: map[string]*types.Backend{ "foo/bar": { Servers: map[string]types.Server{ - "1": { - URL: "http://10.0.0.1:801", + "http://10.10.0.1:8080": { + URL: "http://10.10.0.1:8080", + Weight: 1, + }, + "http://10.21.0.1:8080": { + URL: "http://10.21.0.1:8080", Weight: 1, }, }, @@ -128,8 +220,12 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.0.0.2:802", Weight: 1, }, - "3": { - URL: "https://10.0.0.3:443", + "https://10.15.0.1:8443": { + URL: "https://10.15.0.1:8443", + Weight: 1, + }, + "https://10.15.0.2:9443": { + URL: "https://10.15.0.2:9443", Weight: 1, }, }, @@ -320,7 +416,7 @@ func TestRuleType(t *testing.T) { services: services, watchChan: watchChan, } - provider := Kubernetes{disablePassHostHeaders: true} + provider := Kubernetes{DisablePassHostHeaders: true} actualConfig, err := provider.loadIngresses(client) actual := actualConfig.Frontends if err != nil { @@ -442,7 +538,7 @@ func TestGetPassHostHeader(t *testing.T) { services: services, watchChan: watchChan, } - provider := Kubernetes{disablePassHostHeaders: true} + provider := Kubernetes{DisablePassHostHeaders: true} actual, err := provider.loadIngresses(client) if err != nil { t.Fatalf("error %+v", err) @@ -1060,9 +1156,97 @@ func TestLoadMultipleNamespacedIngresses(t *testing.T) { } } +func TestHostlessIngress(t *testing.T) { + ingresses := []k8s.Ingress{{ + ObjectMeta: k8s.ObjectMeta{ + Namespace: "awesome", + }, + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/bar", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + }, + }, + }} + services := []k8s.Service{ + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service1", + Namespace: "awesome", + UID: "1", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.1", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 801, + }, + }, + }, + }, + } + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + watchChan: watchChan, + } + provider := Kubernetes{DisablePassHostHeaders: true} + actual, err := provider.loadIngresses(client) + if err != nil { + t.Fatalf("error %+v", err) + } + + expected := &types.Configuration{ + Backends: map[string]*types.Backend{ + "/bar": { + Servers: map[string]types.Server{ + "1": { + URL: "http://10.0.0.1:801", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + Frontends: map[string]*types.Frontend{ + "/bar": { + Backend: "/bar", + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefix:/bar", + }, + }, + }, + }, + } + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(expected) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON)) + } +} + type clientMock struct { ingresses []k8s.Ingress services []k8s.Service + endpoints []k8s.Endpoints watchChan chan interface{} } @@ -1078,15 +1262,24 @@ func (c clientMock) GetIngresses(predicate func(k8s.Ingress) bool) ([]k8s.Ingres func (c clientMock) WatchIngresses(predicate func(k8s.Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { return c.watchChan, make(chan error), nil } -func (c clientMock) GetServices(predicate func(k8s.Service) bool) ([]k8s.Service, error) { - var services []k8s.Service +func (c clientMock) GetService(name, namespace string) (k8s.Service, error) { for _, service := range c.services { - if predicate(service) { - services = append(services, service) + if service.Namespace == namespace && service.Name == name { + return service, nil } } - return services, nil + return k8s.Service{}, nil } + +func (c clientMock) GetEndpoints(name, namespace string) (k8s.Endpoints, error) { + for _, endpoints := range c.endpoints { + if endpoints.Namespace == namespace && endpoints.Name == name { + return endpoints, nil + } + } + return k8s.Endpoints{}, nil +} + func (c clientMock) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) { return c.watchChan, make(chan error), nil } diff --git a/provider/kv.go b/provider/kv.go index b257fd71e..42181718a 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -22,20 +22,20 @@ import ( // Kv holds common configurations of key-value providers. type Kv struct { - BaseProvider `mapstructure:",squash"` - Endpoint string - Prefix string - TLS *KvTLS - storeType store.Backend - kvclient store.Store + BaseProvider + Endpoint string `description:"Comma sepparated server endpoints"` + Prefix string `description:"Prefix used for KV store"` + TLS *KvTLS `description:"Enable TLS support"` + storeType store.Backend + kvclient store.Store } // KvTLS holds TLS specific configurations type KvTLS struct { - CA string - Cert string - Key string - InsecureSkipVerify bool + CA string `description:"TLS CA"` + Cert string `description:"TLS cert"` + Key string `description:"TLS key"` + InsecureSkipVerify bool `description:"TLS insecure skip verify"` } func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) error { @@ -73,7 +73,7 @@ func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix return nil } -func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { storeConfig := &store.Config{ ConnectionTimeout: 30 * time.Second, Bucket: "traefik", diff --git a/provider/marathon.go b/provider/marathon.go index 63ba9d8c0..a97e70ecb 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -3,6 +3,7 @@ package provider import ( "errors" "net/url" + "sort" "strconv" "strings" "text/template" @@ -20,13 +21,14 @@ import ( // Marathon holds configuration of the Marathon provider. type Marathon struct { - BaseProvider `mapstructure:",squash"` - Endpoint string - Domain string - ExposedByDefault bool - Basic *MarathonBasic - TLS *tls.Config - marathonClient marathon.Marathon + BaseProvider + Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon"` + Domain string `description:"Default domain used"` + ExposedByDefault bool `description:"Expose Marathon apps by default"` + GroupsAsSubDomains bool `description:"Convert Marathon groups to subdomains"` + Basic *MarathonBasic + TLS *tls.Config + marathonClient marathon.Marathon } // MarathonBasic holds basic authentication specific configurations @@ -36,13 +38,14 @@ type MarathonBasic struct { } type lightMarathonClient interface { - Applications(url.Values) (*marathon.Applications, error) AllTasks(v url.Values) (*marathon.Tasks, error) + Applications(url.Values) (*marathon.Applications, error) } // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { + provider.Constraints = append(provider.Constraints, constraints...) operation := func() error { config := marathon.NewDefaultConfig() config.URL = provider.Endpoint @@ -340,7 +343,7 @@ func (provider *Marathon) getFrontendRule(application marathon.Application) stri if label, err := provider.getLabel(application, "traefik.frontend.rule"); err == nil { return label } - return "Host:" + getEscapedName(application.ID) + "." + provider.Domain + return "Host:" + provider.getSubDomain(application.ID) + "." + provider.Domain } func (provider *Marathon) getBackend(task marathon.Task, applications []marathon.Application) string { @@ -358,3 +361,13 @@ func (provider *Marathon) getFrontendBackend(application marathon.Application) s } return replace("/", "-", application.ID) } + +func (provider *Marathon) getSubDomain(name string) string { + if provider.GroupsAsSubDomains { + splitedName := strings.Split(strings.TrimPrefix(name, "/"), "/") + sort.Sort(sort.Reverse(sort.StringSlice(splitedName))) + reverseName := strings.Join(splitedName, ".") + return reverseName + } + return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) +} diff --git a/provider/provider.go b/provider/provider.go index 3fa7612ec..5f52988e0 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -5,25 +5,45 @@ import ( "io/ioutil" "strings" "text/template" + "unicode" "github.com/BurntSushi/toml" "github.com/containous/traefik/autogen" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" - "unicode" ) // Provider defines methods of a provider. type Provider interface { // Provide allows the provider to provide configurations to traefik // using the given configuration channel. - Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error + Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error } // BaseProvider should be inherited by providers type BaseProvider struct { - Watch bool - Filename string + Watch bool `description:"Watch provider"` + Filename string `description:"Override default configuration template. For advanced users :)"` + Constraints types.Constraints `description:"Filter services by constraint, matching with Traefik tags."` +} + +// MatchConstraints must match with EVERY single contraint +// returns first constraint that do not match or nil +func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) { + // if there is no tags and no contraints, filtering is disabled + if len(tags) == 0 && len(p.Constraints) == 0 { + return true, nil + } + + for _, constraint := range p.Constraints { + // xor: if ok and constraint.MustMatch are equal, then no tag is currently matching with the constraint + if ok := constraint.MatchConstraintWithAtLeastOneTag(tags); ok != constraint.MustMatch { + return false, &constraint + } + } + + // If no constraint or every constraints matching + return true, nil } func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { @@ -65,11 +85,6 @@ func replace(s1 string, s2 string, s3 string) string { return strings.Replace(s3, s1, s2, -1) } -// Escape beginning slash "/", convert all others to dash "-" -func getEscapedName(name string) string { - return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1) -} - func normalize(name string) string { fargs := func(c rune) bool { return !unicode.IsLetter(c) && !unicode.IsNumber(c) diff --git a/provider/provider_test.go b/provider/provider_test.go index b76f5e6bd..7b7e487d9 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" "text/template" + + "github.com/containous/traefik/types" ) type myProvider struct { @@ -206,3 +208,100 @@ func TestGetConfigurationReturnsCorrectMaxConnConfiguration(t *testing.T) { t.Fatalf("Configuration did not parse MaxConn.ExtractorFunc properly") } } + +func TestMatchingConstraints(t *testing.T) { + cases := []struct { + constraints []types.Constraint + tags []string + expected bool + }{ + // simple test: must match + { + constraints: []types.Constraint{ + { + Key: "tag", + MustMatch: true, + Regex: "us-east-1", + }, + }, + tags: []string{ + "us-east-1", + }, + expected: true, + }, + // simple test: must match but does not match + { + constraints: []types.Constraint{ + { + Key: "tag", + MustMatch: true, + Regex: "us-east-1", + }, + }, + tags: []string{ + "us-east-2", + }, + expected: false, + }, + // simple test: must not match + { + constraints: []types.Constraint{ + { + Key: "tag", + MustMatch: false, + Regex: "us-east-1", + }, + }, + tags: []string{ + "us-east-1", + }, + expected: false, + }, + // complex test: globbing + { + constraints: []types.Constraint{ + { + Key: "tag", + MustMatch: true, + Regex: "us-east-*", + }, + }, + tags: []string{ + "us-east-1", + }, + expected: true, + }, + // complex test: multiple constraints + { + constraints: []types.Constraint{ + { + Key: "tag", + MustMatch: true, + Regex: "us-east-*", + }, + { + Key: "tag", + MustMatch: false, + Regex: "api", + }, + }, + tags: []string{ + "api", + "us-east-1", + }, + expected: false, + }, + } + + for i, c := range cases { + provider := myProvider{ + BaseProvider{ + Constraints: c.constraints, + }, + } + actual, _ := provider.MatchConstraints(c.tags) + if actual != c.expected { + t.Fatalf("test #%v: expected %q, got %q, for %q", i, c.expected, actual, c.constraints) + } + } +} diff --git a/provider/zk.go b/provider/zk.go index 77b28100f..06eb65000 100644 --- a/provider/zk.go +++ b/provider/zk.go @@ -14,8 +14,8 @@ 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, pool *safe.Pool) error { +func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []types.Constraint) error { provider.storeType = store.ZK zookeeper.Register() - return provider.provide(configurationChan, pool) + return provider.provide(configurationChan, pool, constraints) } diff --git a/script/deploy-pr.sh b/script/deploy-pr.sh new file mode 100755 index 000000000..7fd79407c --- /dev/null +++ b/script/deploy-pr.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +if ([ "$TRAVIS_BRANCH" = "master" ] && [ -z "$TRAVIS_TAG" ]) && [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$DOCKER_VERSION" = "1.10.1" ]; then + echo "Deploying PR..." +else + echo "Skipping deploy PR" + exit 0 +fi + +COMMENT=$(git log -1 --pretty=%B) +PR=$(echo $COMMENT | grep -oP "Merge pull request #\K(([0-9]*))(?=.*)") + +if [ -z "$PR" ]; then + echo "Unable to get PR number: $PR from: $COMMENT" + exit 0 +fi + +# create docker image containous/traefik +echo "Updating docker containous/traefik image..." +docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS +docker tag containous/traefik containous/traefik:pr-${PR} +docker push containous/traefik:pr-${PR} + +echo "Deployed" diff --git a/script/deploy.sh b/script/deploy.sh index 97f0f2abd..bf2d1bb7f 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if ([ "$TRAVIS_BRANCH" = "master" ] || [ ! -z "$TRAVIS_TAG" ]) && [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$DOCKER_VERSION" = "1.10.1" ]; then +if [ -n "$TRAVIS_TAG" ] && [ "$DOCKER_VERSION" = "1.10.1" ]; then echo "Deploying..." else echo "Skipping deploy" diff --git a/server.go b/server.go index 39a09c271..14694efbe 100644 --- a/server.go +++ b/server.go @@ -68,8 +68,8 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { server := new(Server) server.serverEntryPoints = make(map[string]*serverEntryPoint) - server.configurationChan = make(chan types.ConfigMessage, 10) - server.configurationValidatedChan = make(chan types.ConfigMessage, 10) + server.configurationChan = make(chan types.ConfigMessage, 100) + server.configurationValidatedChan = make(chan types.ConfigMessage, 100) server.signals = make(chan os.Signal, 1) server.stopChan = make(chan bool, 1) server.providers = []provider.Provider{} @@ -248,7 +248,7 @@ func (server *Server) startProviders() { log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf) currentProvider := provider safe.Go(func() { - err := currentProvider.Provide(server.configurationChan, &server.routinesPool) + err := currentProvider.Provide(server.configurationChan, &server.routinesPool, server.globalConfiguration.Constraints) if err != nil { log.Errorf("Error starting provider %s", err) } diff --git a/templates/docker.tmpl b/templates/docker.tmpl index f43bef8b4..08e1f54b5 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -1,6 +1,6 @@ [backends]{{range .Containers}} [backends.backend-{{getBackend .}}.servers.server-{{.Name | replace "/" "" | replace "." "-"}}] - url = "{{getProtocol .}}://{{range $i := .NetworkSettings.Networks}}{{if $i}}{{.IPAddress}}{{end}}{{end}}:{{getPort .}}" + url = "{{getProtocol .}}://{{getIPAddress .}}:{{getPort .}}" weight = {{getWeight .}} {{end}} diff --git a/traefik.go b/traefik.go index dec978a6f..a8be2edd1 100644 --- a/traefik.go +++ b/traefik.go @@ -1,16 +1,179 @@ package main import ( + "encoding/json" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/containous/flaeg" + "github.com/containous/staert" + "github.com/containous/traefik/acme" + "github.com/containous/traefik/middlewares" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/types" fmtlog "log" + "net/http" "os" + "reflect" "runtime" + "strings" + "text/template" ) +var versionTemplate = `Version: {{.Version}} +Go version: {{.GoVersion}} +Built: {{.BuildTime}} +OS/Arch: {{.Os}}/{{.Arch}}` + func main() { runtime.GOMAXPROCS(runtime.NumCPU()) - if err := traefikCmd.Execute(); err != nil { + + //traefik config inits + traefikConfiguration := NewTraefikConfiguration() + traefikPointersConfiguration := NewTraefikDefaultPointersConfiguration() + //traefik Command init + traefikCmd := &flaeg.Command{ + Name: "traefik", + Description: `traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. +Complete documentation is available at https://traefik.io`, + Config: traefikConfiguration, + DefaultPointersConfig: traefikPointersConfiguration, + Run: func() error { + run(traefikConfiguration) + return nil + }, + } + + //version Command init + versionCmd := &flaeg.Command{ + Name: "version", + Description: `Print version`, + Config: struct{}{}, + DefaultPointersConfig: struct{}{}, + Run: func() error { + tmpl, err := template.New("").Parse(versionTemplate) + if err != nil { + return err + } + + v := struct { + Version string + GoVersion string + BuildTime string + Os string + Arch string + }{ + Version: Version, + GoVersion: runtime.Version(), + BuildTime: BuildDate, + Os: runtime.GOOS, + Arch: runtime.GOARCH, + } + + if err := tmpl.Execute(os.Stdout, v); err != nil { + return err + } + fmt.Printf("\n") + return nil + + }, + } + + //init flaeg source + f := flaeg.New(traefikCmd, os.Args[1:]) + //add custom parsers + f.AddParser(reflect.TypeOf(EntryPoints{}), &EntryPoints{}) + f.AddParser(reflect.TypeOf(DefaultEntryPoints{}), &DefaultEntryPoints{}) + f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{}) + f.AddParser(reflect.TypeOf(provider.Namespaces{}), &provider.Namespaces{}) + f.AddParser(reflect.TypeOf([]acme.Domain{}), &acme.Domains{}) + + //add version command + f.AddCommand(versionCmd) + if _, err := f.Parse(traefikCmd); err != nil { fmtlog.Println(err) os.Exit(-1) } + + //staert init + s := staert.NewStaert(traefikCmd) + //init toml source + toml := staert.NewTomlSource("traefik", []string{traefikConfiguration.ConfigFile, "/etc/traefik/", "$HOME/.traefik/", "."}) + + //add sources to staert + s.AddSource(toml) + s.AddSource(f) + if _, err := s.LoadConfig(); err != nil { + fmtlog.Println(err) + } + + traefikConfiguration.ConfigFile = toml.ConfigFileUsed() + + if err := s.Run(); err != nil { + fmtlog.Println(err) + os.Exit(-1) + } + os.Exit(0) } + +func run(traefikConfiguration *TraefikConfiguration) { + fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags) + + // load global configuration + globalConfiguration := traefikConfiguration.GlobalConfiguration + + http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = globalConfiguration.MaxIdleConnsPerHost + loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile) + defer loggerMiddleware.Close() + + if globalConfiguration.File != nil && len(globalConfiguration.File.Filename) == 0 { + // no filename, setting to global config file + if len(traefikConfiguration.ConfigFile) != 0 { + globalConfiguration.File.Filename = traefikConfiguration.ConfigFile + } else { + log.Errorln("Error using file configuration backend, no filename defined") + } + } + + if len(globalConfiguration.EntryPoints) == 0 { + globalConfiguration.EntryPoints = map[string]*EntryPoint{"http": {Address: ":80"}} + globalConfiguration.DefaultEntryPoints = []string{"http"} + } + + if globalConfiguration.Debug { + globalConfiguration.LogLevel = "DEBUG" + } + + // 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 func() { + if err := fi.Close(); err != nil { + log.Error("Error closinf file", err) + } + }() + 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}) + } + jsonConf, _ := json.Marshal(globalConfiguration) + log.Infof("Traefik version %s built on %s", Version, BuildDate) + if len(traefikConfiguration.ConfigFile) != 0 { + log.Infof("Using TOML configuration file %s", traefikConfiguration.ConfigFile) + } + log.Debugf("Global configuration loaded %s", string(jsonConf)) + server := NewServer(globalConfiguration) + server.Start() + defer server.Close() + log.Info("Shutting down") +} diff --git a/traefik.sample.toml b/traefik.sample.toml index 51f2442e4..3b900c112 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -306,7 +306,16 @@ # Optional # Default: false # -# ExposedByDefault = true +# exposedByDefault = true + +# Convert Marathon groups to subdomains +# Default behavior: /foo/bar/myapp => foo-bar-myapp.{defaultDomain} +# with groupsAsSubDomains enabled: /foo/bar/myapp => myapp.bar.foo.{defaultDomain} +# +# Optional +# Default: false +# +# groupsAsSubDomains = true # Enable Marathon basic authentication # diff --git a/types/types.go b/types/types.go index eeedcb73b..dcac6bd46 100644 --- a/types/types.go +++ b/types/types.go @@ -2,6 +2,8 @@ package types import ( "errors" + "fmt" + "github.com/ryanuber/go-glob" "strings" ) @@ -93,3 +95,94 @@ type ConfigMessage struct { ProviderName string Configuration *Configuration } + +// Constraint hold a parsed constraint expresssion +type Constraint struct { + Key string + // MustMatch is true if operator is "==" or false if operator is "!=" + MustMatch bool + // TODO: support regex + Regex string +} + +// NewConstraint receive a string and return a *Constraint, after checking syntax and parsing the constraint expression +func NewConstraint(exp string) (*Constraint, error) { + sep := "" + constraint := &Constraint{} + + if strings.Contains(exp, "==") { + sep = "==" + constraint.MustMatch = true + } else if strings.Contains(exp, "!=") { + sep = "!=" + constraint.MustMatch = false + } else { + return nil, errors.New("Constraint expression missing valid operator: '==' or '!='") + } + + kv := strings.SplitN(exp, sep, 2) + if len(kv) == 2 { + // At the moment, it only supports tags + if kv[0] != "tag" { + return nil, errors.New("Constraint must be tag-based. Syntax: tag==us-*") + } + + constraint.Key = kv[0] + constraint.Regex = kv[1] + return constraint, nil + } + + return nil, errors.New("Incorrect constraint expression: " + exp) +} + +func (c *Constraint) String() string { + if c.MustMatch { + return c.Key + "==" + c.Regex + } + return c.Key + "!=" + c.Regex +} + +// MatchConstraintWithAtLeastOneTag tests a constraint for one single service +func (c *Constraint) MatchConstraintWithAtLeastOneTag(tags []string) bool { + for _, tag := range tags { + if glob.Glob(c.Regex, tag) { + return true + } + } + return false +} + +//Set []*Constraint +func (cs *Constraints) Set(str string) error { + exps := strings.Split(str, ",") + if len(exps) == 0 { + return errors.New("Bad Constraint format: " + str) + } + for _, exp := range exps { + constraint, err := NewConstraint(exp) + if err != nil { + return err + } + *cs = append(*cs, *constraint) + } + return nil +} + +// Constraints holds a Constraint parser +type Constraints []Constraint + +//Get []*Constraint +func (cs *Constraints) Get() interface{} { return []Constraint(*cs) } + +//String returns []*Constraint in string +func (cs *Constraints) String() string { return fmt.Sprintf("%+v", *cs) } + +//SetValue sets []*Constraint into the parser +func (cs *Constraints) SetValue(val interface{}) { + *cs = Constraints(val.(Constraints)) +} + +// Type exports the Constraints type as a string +func (cs *Constraints) Type() string { + return fmt.Sprint("constraint") +} diff --git a/version.go b/version.go index 15a6ee9dd..10f7cdb3c 100644 --- a/version.go +++ b/version.go @@ -2,7 +2,7 @@ package main var ( // Version holds the current version of traefik. - Version = "" + Version = "dev" // BuildDate holds the build date of traefik. - BuildDate = "" + BuildDate = "I don't remember exactly" ) diff --git a/web.go b/web.go index 445a83bce..690c2ecfc 100644 --- a/web.go +++ b/web.go @@ -2,9 +2,11 @@ package main import ( "encoding/json" + "expvar" "fmt" "io/ioutil" "net/http" + "runtime" log "github.com/Sirupsen/logrus" "github.com/containous/traefik/autogen" @@ -21,10 +23,11 @@ var metrics = stats.New() // WebProvider is a provider.Provider implementation that provides the UI. // FIXME to be handled another way. type WebProvider struct { - Address string - CertFile, KeyFile string - ReadOnly bool - server *Server + Address string `description:"Web administration port"` + CertFile string `description:"SSL certificate"` + KeyFile string `description:"SSL certificate"` + ReadOnly bool `description:"Enable read only API"` + server *Server } var ( @@ -33,9 +36,17 @@ var ( }) ) +func init() { + expvar.Publish("Goroutines", expvar.Func(goroutines)) +} + +func goroutines() interface{} { + return runtime.NumGoroutine() +} + // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ []types.Constraint) error { systemRouter := mux.NewRouter() // health route @@ -82,7 +93,12 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag systemRouter.Methods("GET").Path("/").HandlerFunc(func(response http.ResponseWriter, request *http.Request) { http.Redirect(response, request, "/dashboard/", 302) }) - systemRouter.Methods("GET").PathPrefix("/dashboard/").Handler(http.StripPrefix("/dashboard/", http.FileServer(&assetfs.AssetFS{Asset: autogen.Asset, AssetDir: autogen.AssetDir, Prefix: "static"}))) + systemRouter.Methods("GET").PathPrefix("/dashboard/").Handler(http.StripPrefix("/dashboard/", http.FileServer(&assetfs.AssetFS{Asset: autogen.Asset, AssetInfo: autogen.AssetInfo, AssetDir: autogen.AssetDir, Prefix: "static"}))) + + // expvars + if provider.server.globalConfiguration.Debug { + systemRouter.Methods("GET").Path("/debug/vars").HandlerFunc(expvarHandler) + } go func() { if len(provider.CertFile) > 0 && len(provider.KeyFile) > 0 { @@ -231,3 +247,17 @@ func (provider *WebProvider) getRouteHandler(response http.ResponseWriter, reque } http.NotFound(response, request) } + +func expvarHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, "{\n") + first := true + expvar.Do(func(kv expvar.KeyValue) { + if !first { + fmt.Fprintf(w, ",\n") + } + first = false + fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) + }) + fmt.Fprintf(w, "\n}\n") +}