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