From ac087921d8afd89dc4f6eebdeeabb98272c72f82 Mon Sep 17 00:00:00 2001 From: Samuel BERTHE Date: Mon, 30 May 2016 15:05:58 +0200 Subject: [PATCH 1/5] feat(constraints): Implementation of constraint filtering (cmd + toml + matching functions), implementation proposal with consul --- configuration.go | 2 + glide.lock | 5 +- glide.yaml | 1 + provider/boltdb.go | 4 +- provider/consul.go | 4 +- provider/consul_catalog.go | 43 ++++++++++--- provider/docker.go | 3 +- provider/etcd.go | 4 +- provider/file.go | 2 +- provider/kubernetes.go | 3 +- provider/kv.go | 2 +- provider/marathon.go | 3 +- provider/provider.go | 30 +++++++++- provider/zk.go | 4 +- server.go | 2 +- types/types.go | 120 +++++++++++++++++++++++++++++++++++++ web.go | 2 +- 17 files changed, 209 insertions(+), 25 deletions(-) diff --git a/configuration.go b/configuration.go index 9949d3cf3..ee996b693 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 []*types.Constraint `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."` @@ -294,6 +295,7 @@ func NewTraefikConfiguration() *TraefikConfiguration { TraefikLogsFile: "", LogLevel: "ERROR", EntryPoints: map[string]*EntryPoint{}, + Constraints: []*Constraint, DefaultEntryPoints: []string{}, ProvidersThrottleDuration: time.Duration(2 * time.Second), MaxIdleConnsPerHost: 200, diff --git a/glide.lock b/glide.lock index cdc1a522d..1f95df545 100644 --- a/glide.lock +++ b/glide.lock @@ -213,4 +213,7 @@ imports: subpackages: - cipher - json -devImports: [] +- name: gopkg.in/yaml.v2 + version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 +- name: github.com/ryanuber/go-glob + version: 572520ed46dbddaed19ea3d9541bdd0494163693 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/provider/boltdb.go b/provider/boltdb.go index 4c2a33844..50b1fa36a 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..f74490371 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..ec82ecb39 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -90,13 +90,25 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro set := map[string]bool{} tags := []string{} + nodes := []*api.ServiceEntry{} for _, node := range data { - for _, tag := range node.Service.Tags { - if _, ok := set[tag]; ok == false { - set[tag] = true - tags = append(tags, tag) + constraintTags := provider.getContraintTags(node.Service.Tags) + if ok, failingConstraint, err := provider.MatchConstraints(constraintTags); err != nil { + return catalogUpdate{}, err + } else if ok == true { + nodes = append(nodes, node) + // merge tags of every nodes in a single slice + // only if node match constraint + for _, tag := range node.Service.Tags { + if _, ok := set[tag]; ok == false { + set[tag] = true + tags = append(tags, tag) + } } + } else if ok == false && failingConstraint != nil { + log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String()) } + } return catalogUpdate{ @@ -104,7 +116,7 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro ServiceName: service, Attributes: tags, }, - Nodes: data, + Nodes: nodes, }, nil } @@ -157,6 +169,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 +237,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 +276,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 +284,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 453c33476..2565d1a84 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..35f493d03 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..b1038df77 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..0681ff24b 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..3791dd3b4 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..cf5e96622 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..5355ae3d8 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -5,25 +5,46 @@ 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 :)"` + 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 +// returns errors for future use (regex) +func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint, error) { + // if there is no tags and no contraints, filtering is disabled + if len(tags) == 0 && len(p.Constraints) == 0 { + return true, nil, nil + } + + for _, constraint := range p.Constraints { + if ok := constraint.MatchConstraintWithAtLeastOneTag(tags); xor(ok == true, constraint.MustMatch == true) { + return false, constraint, nil + } + } + + // If no constraint or every constraints matching + return true, nil, nil +>>>>>>> e844462... feat(constraints): Implementation of constraints (cmd + toml + matching functions), implementation proposal with consul } func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { @@ -77,3 +98,8 @@ func normalize(name string) string { // get function return strings.Join(strings.FieldsFunc(name, fargs), "-") } + +// golang does not support ^ operator +func xor(cond1 bool, cond2 bool) bool { + return cond1 != cond2 +} diff --git a/provider/zk.go b/provider/zk.go index 77b28100f..7d5562ee5 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/types/types.go b/types/types.go index eeedcb73b..5eaa1e51e 100644 --- a/types/types.go +++ b/types/types.go @@ -2,6 +2,10 @@ package types import ( "errors" + "fmt" + "github.com/mitchellh/mapstructure" + "github.com/ryanuber/go-glob" + "reflect" "strings" ) @@ -93,3 +97,119 @@ 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 + Regex string +} + +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 +} + +func (c *Constraint) MatchConstraintWithAtLeastOneTag(tags []string) bool { + for _, tag := range tags { + if glob.Glob(c.Regex, tag) { + return true + } + } + return false +} + +// StringToConstraintHookFunc returns a DecodeHookFunc that converts strings to Constraint. +// This hook is triggered during the configuration file unmarshal-ing +func StringToConstraintHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(&Constraint{}) { + return data, nil + } + + if constraint, err := NewConstraint(data.(string)); err != nil { + return data, err + } else { + return constraint, nil + } + } +} + +type Constraints struct { + value *[]*Constraint + changed bool +} + +// Command line +func (cs *Constraints) Set(value string) error { + exps := strings.Split(value, ",") + if len(exps) == 0 { + return errors.New("Bad Constraint format: " + value) + } + for _, exp := range exps { + if constraint, err := NewConstraint(exp); err != nil { + return err + } else { + *cs.value = append(*cs.value, constraint) + } + } + return nil +} + +func (c *Constraints) Type() string { + return "constraints" +} + +func (c *Constraints) String() string { + return fmt.Sprintln("%v", *c.value) +} + +// NewConstraintSliceValue make an alias of []*Constraint to Constraints for the command line +// Viper does not supprt SliceVar value types +// Constraints.Set called by viper will fill the []*Constraint slice +func NewConstraintSliceValue(p *[]*Constraint) *Constraints { + cs := new(Constraints) + cs.value = p + if p == nil { + *cs.value = []*Constraint{} + } + return cs +} diff --git a/web.go b/web.go index f402edfec..7cafdcc0d 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 From cd2100ed84f40b824d44e0435ebbb2d25c78558b Mon Sep 17 00:00:00 2001 From: Samuel BERTHE Date: Fri, 20 May 2016 16:43:56 +0200 Subject: [PATCH 2/5] doc(constraints): Added in ConsulCatalog backend + new 'Constraint' section --- docs/toml.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/toml.md b/docs/toml.md index 0d71cd995..7355cf6ba 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 @@ -741,6 +786,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 From f46accc74d09f43651bbc8b2ad1058720e35fd7c Mon Sep 17 00:00:00 2001 From: Samuel BERTHE Date: Fri, 20 May 2016 17:17:38 +0200 Subject: [PATCH 3/5] test(constraint): unit tests + integration tests + make validate --- integration/constraint_test.go | 211 ++++++++++++++++++ integration/integration_test.go | 1 + integration/resources/compose/constraints.yml | 17 ++ provider/consul_catalog.go | 31 ++- provider/provider.go | 18 +- provider/provider_test.go | 99 ++++++++ types/types.go | 25 ++- 7 files changed, 362 insertions(+), 40 deletions(-) create mode 100644 integration/constraint_test.go create mode 100644 integration/resources/compose/constraints.yml diff --git a/integration/constraint_test.go b/integration/constraint_test.go new file mode 100644 index 000000000..50e194d2e --- /dev/null +++ b/integration/constraint_test.go @@ -0,0 +1,211 @@ +package main + +import ( + //"io/ioutil" + //"fmt" + "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/consul_catalog.go b/provider/consul_catalog.go index ec82ecb39..0b0d2294b 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,28 +89,22 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro return catalogUpdate{}, err } - set := map[string]bool{} - tags := []string{} - nodes := []*api.ServiceEntry{} - for _, node := range data { + nodes := fun.Filter(func(node *api.ServiceEntry) bool { constraintTags := provider.getContraintTags(node.Service.Tags) - if ok, failingConstraint, err := provider.MatchConstraints(constraintTags); err != nil { - return catalogUpdate{}, err - } else if ok == true { - nodes = append(nodes, node) - // merge tags of every nodes in a single slice - // only if node match constraint - for _, tag := range node.Service.Tags { - if _, ok := set[tag]; ok == false { - set[tag] = true - tags = append(tags, tag) - } - } - } else if ok == false && failingConstraint != nil { + 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{ diff --git a/provider/provider.go b/provider/provider.go index 5355ae3d8..43d662401 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -29,22 +29,21 @@ type BaseProvider struct { // MatchConstraints must match with EVERY single contraint // returns first constraint that do not match or nil -// returns errors for future use (regex) -func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint, error) { +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, nil + return true, nil } for _, constraint := range p.Constraints { - if ok := constraint.MatchConstraintWithAtLeastOneTag(tags); xor(ok == true, constraint.MustMatch == true) { - return false, constraint, nil + // 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, nil ->>>>>>> e844462... feat(constraints): Implementation of constraints (cmd + toml + matching functions), implementation proposal with consul + return true, nil } func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) { @@ -98,8 +97,3 @@ func normalize(name string) string { // get function return strings.Join(strings.FieldsFunc(name, fargs), "-") } - -// golang does not support ^ operator -func xor(cond1 bool, cond2 bool) bool { - return cond1 != cond2 -} diff --git a/provider/provider_test.go b/provider/provider_test.go index b76f5e6bd..824d3ced0 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/types/types.go b/types/types.go index 5eaa1e51e..2ce5ceb97 100644 --- a/types/types.go +++ b/types/types.go @@ -106,6 +106,7 @@ type Constraint struct { 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{} @@ -142,6 +143,7 @@ func (c *Constraint) String() string { 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) { @@ -165,41 +167,44 @@ func StringToConstraintHookFunc() mapstructure.DecodeHookFunc { return data, nil } - if constraint, err := NewConstraint(data.(string)); err != nil { + constraint, err := NewConstraint(data.(string)) + if err != nil { return data, err - } else { - return constraint, nil } + return constraint, nil } } +// Constraints own a pointer on globalConfiguration.Constraints and supports a Set() method (not possible on a slice) +// interface: type Constraints struct { value *[]*Constraint changed bool } -// Command line +// Set receive a cli argument and add it to globalConfiguration func (cs *Constraints) Set(value string) error { exps := strings.Split(value, ",") if len(exps) == 0 { return errors.New("Bad Constraint format: " + value) } for _, exp := range exps { - if constraint, err := NewConstraint(exp); err != nil { + constraint, err := NewConstraint(exp) + if err != nil { return err - } else { - *cs.value = append(*cs.value, constraint) } + *cs.value = append(*cs.value, constraint) } return nil } -func (c *Constraints) Type() string { +// Type exports the Constraints type as a string +func (cs *Constraints) Type() string { return "constraints" } -func (c *Constraints) String() string { - return fmt.Sprintln("%v", *c.value) +func (cs *Constraints) String() string { + return fmt.Sprintln("%v", *cs.value) } // NewConstraintSliceValue make an alias of []*Constraint to Constraints for the command line From 1de5434e1ad5a2a8f04e04ab8a7b464894d276ec Mon Sep 17 00:00:00 2001 From: Samuel BERTHE Date: Tue, 31 May 2016 09:54:42 +0200 Subject: [PATCH 4/5] refacto(constraints): Migration to Flaeg cli library --- configuration.go | 46 +++++++++++++++++++++- glide.lock | 3 +- integration/constraint_test.go | 2 - provider/boltdb.go | 2 +- provider/consul.go | 2 +- provider/consul_catalog.go | 2 +- provider/docker.go | 2 +- provider/etcd.go | 2 +- provider/file.go | 2 +- provider/kubernetes.go | 2 +- provider/kv.go | 2 +- provider/marathon.go | 2 +- provider/provider.go | 10 ++--- provider/provider_test.go | 12 +++--- provider/zk.go | 2 +- server.go | 2 +- traefik.go | 3 ++ types/types.go | 72 +--------------------------------- web.go | 2 +- 19 files changed, 73 insertions(+), 99 deletions(-) diff --git a/configuration.go b/configuration.go index ee996b693..2df24ca82 100644 --- a/configuration.go +++ b/configuration.go @@ -26,7 +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 []*types.Constraint `description:"Filter services by constraint, matching with service tags."` + 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."` @@ -146,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 @@ -232,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 @@ -239,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 @@ -250,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, @@ -295,7 +337,7 @@ func NewTraefikConfiguration() *TraefikConfiguration { TraefikLogsFile: "", LogLevel: "ERROR", EntryPoints: map[string]*EntryPoint{}, - Constraints: []*Constraint, + Constraints: []types.Constraint{}, DefaultEntryPoints: []string{}, ProvidersThrottleDuration: time.Duration(2 * time.Second), MaxIdleConnsPerHost: 200, diff --git a/glide.lock b/glide.lock index 1f95df545..69e24ab89 100644 --- a/glide.lock +++ b/glide.lock @@ -213,7 +213,6 @@ imports: subpackages: - cipher - json -- name: gopkg.in/yaml.v2 - version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 - name: github.com/ryanuber/go-glob version: 572520ed46dbddaed19ea3d9541bdd0494163693 +devImports: [] diff --git a/integration/constraint_test.go b/integration/constraint_test.go index 50e194d2e..66ad8bbc0 100644 --- a/integration/constraint_test.go +++ b/integration/constraint_test.go @@ -1,8 +1,6 @@ package main import ( - //"io/ioutil" - //"fmt" "net/http" "os/exec" "time" diff --git a/provider/boltdb.go b/provider/boltdb.go index 50b1fa36a..574956ace 100644 --- a/provider/boltdb.go +++ b/provider/boltdb.go @@ -14,7 +14,7 @@ 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, constraints []*types.Constraint) 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, constraints) diff --git a/provider/consul.go b/provider/consul.go index f74490371..f936e79f3 100644 --- a/provider/consul.go +++ b/provider/consul.go @@ -14,7 +14,7 @@ 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, constraints []*types.Constraint) 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, constraints) diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index 0b0d2294b..cce6db185 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -271,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, constraints []*types.Constraint) 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) diff --git a/provider/docker.go b/provider/docker.go index 2565d1a84..f04a82c03 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -79,7 +79,7 @@ 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, constraints []*types.Constraint) 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() { diff --git a/provider/etcd.go b/provider/etcd.go index 35f493d03..934e0f245 100644 --- a/provider/etcd.go +++ b/provider/etcd.go @@ -14,7 +14,7 @@ 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, constraints []*types.Constraint) 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, constraints) diff --git a/provider/file.go b/provider/file.go index b1038df77..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, _ []*types.Constraint) 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 0681ff24b..e46b165d4 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -81,7 +81,7 @@ 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, constraints []*types.Constraint) 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 diff --git a/provider/kv.go b/provider/kv.go index 3791dd3b4..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, constraints []*types.Constraint) 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 cf5e96622..efdaaf238 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -42,7 +42,7 @@ 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, constraints []*types.Constraint) 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() diff --git a/provider/provider.go b/provider/provider.go index 43d662401..d983ae7ca 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -17,14 +17,14 @@ import ( 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, constraints []*types.Constraint) 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 :)"` - Constraints []*types.Constraint `description:"Filter services by constraint, matching with Traefik tags."` + 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 @@ -38,7 +38,7 @@ func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) 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 + return false, &constraint } } diff --git a/provider/provider_test.go b/provider/provider_test.go index 824d3ced0..7b7e487d9 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -211,13 +211,13 @@ func TestGetConfigurationReturnsCorrectMaxConnConfiguration(t *testing.T) { func TestMatchingConstraints(t *testing.T) { cases := []struct { - constraints []*types.Constraint + constraints []types.Constraint tags []string expected bool }{ // simple test: must match { - constraints: []*types.Constraint{ + constraints: []types.Constraint{ { Key: "tag", MustMatch: true, @@ -231,7 +231,7 @@ func TestMatchingConstraints(t *testing.T) { }, // simple test: must match but does not match { - constraints: []*types.Constraint{ + constraints: []types.Constraint{ { Key: "tag", MustMatch: true, @@ -245,7 +245,7 @@ func TestMatchingConstraints(t *testing.T) { }, // simple test: must not match { - constraints: []*types.Constraint{ + constraints: []types.Constraint{ { Key: "tag", MustMatch: false, @@ -259,7 +259,7 @@ func TestMatchingConstraints(t *testing.T) { }, // complex test: globbing { - constraints: []*types.Constraint{ + constraints: []types.Constraint{ { Key: "tag", MustMatch: true, @@ -273,7 +273,7 @@ func TestMatchingConstraints(t *testing.T) { }, // complex test: multiple constraints { - constraints: []*types.Constraint{ + constraints: []types.Constraint{ { Key: "tag", MustMatch: true, diff --git a/provider/zk.go b/provider/zk.go index 7d5562ee5..06eb65000 100644 --- a/provider/zk.go +++ b/provider/zk.go @@ -14,7 +14,7 @@ type Zookepper struct { // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints []*types.Constraint) 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, constraints) diff --git a/server.go b/server.go index 14694efbe..6d8605b66 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, server.globalConfiguration.Constraints) + err := currentProvider.Provide(server.configurationChan, &server.routinesPool, server.globalConfiguration.Constraints.Get().([]types.Constraint)) 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 2ce5ceb97..557a31b20 100644 --- a/types/types.go +++ b/types/types.go @@ -2,10 +2,7 @@ package types import ( "errors" - "fmt" - "github.com/mitchellh/mapstructure" "github.com/ryanuber/go-glob" - "reflect" "strings" ) @@ -103,7 +100,8 @@ type Constraint struct { Key string // MustMatch is true if operator is "==" or false if operator is "!=" MustMatch bool - Regex string + // TODO: support regex + Regex string } // NewConstraint receive a string and return a *Constraint, after checking syntax and parsing the constraint expression @@ -152,69 +150,3 @@ func (c *Constraint) MatchConstraintWithAtLeastOneTag(tags []string) bool { } return false } - -// StringToConstraintHookFunc returns a DecodeHookFunc that converts strings to Constraint. -// This hook is triggered during the configuration file unmarshal-ing -func StringToConstraintHookFunc() mapstructure.DecodeHookFunc { - return func( - f reflect.Type, - t reflect.Type, - data interface{}) (interface{}, error) { - if f.Kind() != reflect.String { - return data, nil - } - if t != reflect.TypeOf(&Constraint{}) { - return data, nil - } - - constraint, err := NewConstraint(data.(string)) - if err != nil { - return data, err - } - return constraint, nil - } -} - -// Constraints own a pointer on globalConfiguration.Constraints and supports a Set() method (not possible on a slice) -// interface: -type Constraints struct { - value *[]*Constraint - changed bool -} - -// Set receive a cli argument and add it to globalConfiguration -func (cs *Constraints) Set(value string) error { - exps := strings.Split(value, ",") - if len(exps) == 0 { - return errors.New("Bad Constraint format: " + value) - } - for _, exp := range exps { - constraint, err := NewConstraint(exp) - if err != nil { - return err - } - *cs.value = append(*cs.value, constraint) - } - return nil -} - -// Type exports the Constraints type as a string -func (cs *Constraints) Type() string { - return "constraints" -} - -func (cs *Constraints) String() string { - return fmt.Sprintln("%v", *cs.value) -} - -// NewConstraintSliceValue make an alias of []*Constraint to Constraints for the command line -// Viper does not supprt SliceVar value types -// Constraints.Set called by viper will fill the []*Constraint slice -func NewConstraintSliceValue(p *[]*Constraint) *Constraints { - cs := new(Constraints) - cs.value = p - if p == nil { - *cs.value = []*Constraint{} - } - return cs -} diff --git a/web.go b/web.go index 7cafdcc0d..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, _ []*types.Constraint) error { +func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ []types.Constraint) error { systemRouter := mux.NewRouter() // health route From d297a220ce3289f9aeb8ae9dd44b519590079c5e Mon Sep 17 00:00:00 2001 From: Samuel BERTHE Date: Wed, 1 Jun 2016 10:29:55 +0200 Subject: [PATCH 5/5] fix(constraints): Syntax --- server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.go b/server.go index 6d8605b66..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, server.globalConfiguration.Constraints.Get().([]types.Constraint)) + err := currentProvider.Provide(server.configurationChan, &server.routinesPool, server.globalConfiguration.Constraints) if err != nil { log.Errorf("Error starting provider %s", err) }