feat(constraints): Implementation of constraint filtering (cmd + toml + matching functions), implementation proposal with consul
This commit is contained in:
parent
39fa8f7be4
commit
ac087921d8
17 changed files with 209 additions and 25 deletions
|
@ -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,
|
||||
|
|
5
glide.lock
generated
5
glide.lock
generated
|
@ -213,4 +213,7 @@ imports:
|
|||
subpackages:
|
||||
- cipher
|
||||
- json
|
||||
devImports: []
|
||||
- name: gopkg.in/yaml.v2
|
||||
version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40
|
||||
- name: github.com/ryanuber/go-glob
|
||||
version: 572520ed46dbddaed19ea3d9541bdd0494163693
|
||||
|
|
|
@ -76,3 +76,4 @@ import:
|
|||
version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873
|
||||
- package: github.com/mattn/go-shellwords
|
||||
- package: github.com/vdemeester/shakers
|
||||
- package: github.com/ryanuber/go-glob
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
120
types/types.go
120
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
|
||||
}
|
||||
|
|
2
web.go
2
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
|
||||
|
|
Loading…
Reference in a new issue