diff --git a/configuration.go b/configuration.go index 9949d3cf3..2df24ca82 100644 --- a/configuration.go +++ b/configuration.go @@ -26,6 +26,7 @@ type GlobalConfiguration struct { 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 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."` @@ -145,6 +146,41 @@ func (ep *EntryPoints) Type() string { return fmt.Sprint("entrypoints²") } +// Constraints holds a Constraint parser +type Constraints []types.Constraint + +//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 := types.NewConstraint(exp) + if err != nil { + return err + } + *cs = append(*cs, *constraint) + } + return nil +} + +//Get []*Constraint +func (cs *Constraints) Get() interface{} { return []types.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.([]types.Constraint)) +} + +// Type exports the Constraints type as a string +func (cs *Constraints) Type() string { + return fmt.Sprint("constraint²") +} + // EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...) type EntryPoint struct { Network string @@ -231,6 +267,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { defaultMarathon.Watch = true defaultMarathon.Endpoint = "http://127.0.0.1:8080" defaultMarathon.ExposedByDefault = true + defaultMarathon.Constraints = []types.Constraint{} // default Consul var defaultConsul provider.Consul @@ -238,10 +275,12 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { 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 @@ -249,23 +288,27 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { 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, @@ -294,6 +337,7 @@ func NewTraefikConfiguration() *TraefikConfiguration { TraefikLogsFile: "", LogLevel: "ERROR", EntryPoints: map[string]*EntryPoint{}, + Constraints: []types.Constraint{}, DefaultEntryPoints: []string{}, ProvidersThrottleDuration: time.Duration(2 * time.Second), MaxIdleConnsPerHost: 200, diff --git a/docs/toml.md b/docs/toml.md index c024ac2af..9689fafa6 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 @@ -742,6 +787,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 diff --git a/glide.lock b/glide.lock index cdc1a522d..69e24ab89 100644 --- a/glide.lock +++ b/glide.lock @@ -213,4 +213,6 @@ imports: subpackages: - cipher - json +- name: github.com/ryanuber/go-glob + version: 572520ed46dbddaed19ea3d9541bdd0494163693 devImports: [] diff --git a/glide.yaml b/glide.yaml index 38d0ae099..4636007ae 100644 --- a/glide.yaml +++ b/glide.yaml @@ -76,3 +76,4 @@ import: version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873 - package: github.com/mattn/go-shellwords - package: github.com/vdemeester/shakers +- package: github.com/ryanuber/go-glob 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 4c2a33844..574956ace 100644 --- a/provider/boltdb.go +++ b/provider/boltdb.go @@ -14,8 +14,8 @@ type BoltDb struct { // 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 d94dc7e03..f936e79f3 100644 --- a/provider/consul.go +++ b/provider/consul.go @@ -14,8 +14,8 @@ type Consul struct { // 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 2aecc4622..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" @@ -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 bec410170..542f244ab 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -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 { diff --git a/provider/etcd.go b/provider/etcd.go index a7fd7ae6a..934e0f245 100644 --- a/provider/etcd.go +++ b/provider/etcd.go @@ -14,8 +14,8 @@ type Etcd struct { // 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 1b463593a..07bcbd02f 100644 --- a/provider/file.go +++ b/provider/file.go @@ -19,7 +19,7 @@ type File struct { // 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/kubernetes.go b/provider/kubernetes.go index cab4217ea..e46b165d4 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -81,12 +81,13 @@ 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) { operation := func() error { diff --git a/provider/kv.go b/provider/kv.go index 713416781..42181718a 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -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 91b4a2e8d..efdaaf238 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -42,7 +42,8 @@ type lightMarathonClient interface { // 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 diff --git a/provider/provider.go b/provider/provider.go index eddf5a76c..d983ae7ca 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 `description:"Watch provider"` - Filename string `description:"Override default configuration template. For advanced users :)"` + Watch bool `description:"Watch provider"` + Filename string `description:"Override default configuration template. For advanced users :)"` + Constraints []types.Constraint `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) { 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/server.go b/server.go index c1bb55cd0..14694efbe 100644 --- a/server.go +++ b/server.go @@ -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/traefik.go b/traefik.go index b05b558eb..c440fd39c 100644 --- a/traefik.go +++ b/traefik.go @@ -8,6 +8,7 @@ import ( "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" @@ -52,6 +53,8 @@ Complete documentation is available at https://traefik.io`, //add custom parsers f.AddParser(reflect.TypeOf(EntryPoints{}), &EntryPoints{}) f.AddParser(reflect.TypeOf(DefaultEntryPoints{}), &DefaultEntryPoints{}) + f.AddParser(reflect.TypeOf([]types.Constraint{}), &Constraints{}) + f.AddParser(reflect.TypeOf(Constraints{}), &Constraints{}) f.AddParser(reflect.TypeOf(provider.Namespaces{}), &provider.Namespaces{}) f.AddParser(reflect.TypeOf([]acme.Domain{}), &acme.Domains{}) diff --git a/types/types.go b/types/types.go index eeedcb73b..557a31b20 100644 --- a/types/types.go +++ b/types/types.go @@ -2,6 +2,7 @@ package types import ( "errors" + "github.com/ryanuber/go-glob" "strings" ) @@ -93,3 +94,59 @@ 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 +} diff --git a/web.go b/web.go index f402edfec..690c2ecfc 100644 --- a/web.go +++ b/web.go @@ -46,7 +46,7 @@ func goroutines() interface{} { // 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