From fe68e9e24366d937ab18a4a86c31c4d1a1b97534 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 21 Jun 2019 09:24:04 +0200 Subject: [PATCH] New constraints management. Co-authored-by: Mathieu Lonjaret --- Gopkg.lock | 15 +- Gopkg.toml | 4 - .../getting-started/configuration-overview.md | 4 +- docs/content/operations/cli.md | 6 +- docs/content/providers/docker.md | 52 ++++- docs/content/providers/marathon.md | 67 ++++-- docs/content/providers/overview.md | 86 +++----- docs/content/providers/rancher.md | 101 +++++---- docs/content/providers/rancher.toml | 20 ++ .../reference/static-configuration/cli.txt | 42 +--- .../reference/static-configuration/env.md | 36 +--- .../reference/static-configuration/file.toml | 34 +-- pkg/anonymize/anonymize_config_test.go | 16 +- pkg/config/file/file_node_test.go | 2 - pkg/config/file/fixtures/sample.toml | 1 - pkg/config/file/fixtures/sample.yml | 1 - pkg/provider/constrainer.go | 27 --- pkg/provider/constraints/constraints.go | 100 +++++++++ pkg/provider/constraints/constraints_test.go | 204 ++++++++++++++++++ pkg/provider/docker/config.go | 12 +- pkg/provider/docker/config_test.go | 19 +- pkg/provider/docker/docker.go | 2 +- pkg/provider/docker/label.go | 3 +- pkg/provider/marathon/config.go | 29 ++- pkg/provider/marathon/config_test.go | 54 ++--- pkg/provider/marathon/label.go | 11 +- pkg/provider/marathon/label_test.go | 46 +--- pkg/provider/marathon/marathon.go | 38 ++-- pkg/provider/rancher/config.go | 12 +- pkg/provider/rancher/config_test.go | 19 +- pkg/provider/rancher/label.go | 3 +- pkg/provider/rancher/rancher.go | 3 +- pkg/provider/rest/rest.go | 1 - pkg/types/constraints.go | 88 -------- pkg/types/metrics.go | 1 - vendor/github.com/ryanuber/go-glob/LICENSE | 21 -- vendor/github.com/ryanuber/go-glob/glob.go | 56 ----- vendor/github.com/vulcand/predicate/lib.go | 8 + vendor/github.com/vulcand/predicate/parse.go | 26 +++ .../github.com/vulcand/predicate/predicate.go | 18 ++ 40 files changed, 658 insertions(+), 630 deletions(-) create mode 100644 docs/content/providers/rancher.toml delete mode 100644 pkg/provider/constrainer.go create mode 100644 pkg/provider/constraints/constraints.go create mode 100644 pkg/provider/constraints/constraints_test.go delete mode 100644 pkg/types/constraints.go delete mode 100644 vendor/github.com/ryanuber/go-glob/LICENSE delete mode 100644 vendor/github.com/ryanuber/go-glob/glob.go diff --git a/Gopkg.lock b/Gopkg.lock index ad88768f6..ef3da06b3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1415,14 +1415,6 @@ pruneopts = "NUT" revision = "1f30fe9094a513ce4c700b9a54458bbb0c96996c" -[[projects]] - branch = "master" - digest = "1:09d61699d553a4e6ec998ad29816177b1f3d3ed0c18fe923d2c174ec065c99c8" - name = "github.com/ryanuber/go-glob" - packages = ["."] - pruneopts = "NUT" - revision = "256dc444b735e061061cf46c809487313d5b0065" - [[projects]] digest = "1:253f275bd72c42f8d234712d1574c8b222fe9b72838bfaca11b21ace9c0e3d0a" name = "github.com/sacloud/libsacloud" @@ -1598,12 +1590,12 @@ revision = "3d629cff40b7040e0519628e7774ed11a95d9aff" [[projects]] - digest = "1:ca6bac407fedc14fbeeba861dd33a821ba3a1624c10126ec6003b0a28d4139c5" + digest = "1:b9d8cc221fb40078c7eb78d73b1702b5b548511b3d62bbd56b2f8180089c79af" name = "github.com/vulcand/predicate" packages = ["."] pruneopts = "NUT" - revision = "939c094524d124c55fa8afe0e077701db4a865e2" - version = "v1.0.0" + revision = "8fbfb3ab0e94276b6b58bec378600829adc7a203" + version = "v1.1.0" [[projects]] branch = "master" @@ -2289,7 +2281,6 @@ "github.com/prometheus/client_golang/prometheus/promhttp", "github.com/prometheus/client_model/go", "github.com/rancher/go-rancher-metadata/metadata", - "github.com/ryanuber/go-glob", "github.com/sirupsen/logrus", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/mock", diff --git a/Gopkg.toml b/Gopkg.toml index 7f7fd3244..918ec36be 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -146,10 +146,6 @@ required = [ name = "github.com/rancher/go-rancher-metadata" source = "github.com/containous/go-rancher-metadata" -[[constraint]] - branch = "master" - name = "github.com/ryanuber/go-glob" - [[constraint]] name = "github.com/Masterminds/sprig" version = "2.19.0" diff --git a/docs/content/getting-started/configuration-overview.md b/docs/content/getting-started/configuration-overview.md index 3620123b4..02a574ff7 100644 --- a/docs/content/getting-started/configuration-overview.md +++ b/docs/content/getting-started/configuration-overview.md @@ -10,10 +10,10 @@ Configuration in Traefik can refer to two different things: - The fully dynamic routing configuration (referred to as the _dynamic configuration_) - The startup configuration (referred to as the _static configuration_) -Elements in the _static configuration_ set up connections to [providers](../../providers/overview/) and define the [entrypoints](../../routing/entrypoints/) Traefik will listen to (these elements don't change often). +Elements in the _static configuration_ set up connections to [providers](../providers/overview.md) and define the [entrypoints](../routing/entrypoints.md) Traefik will listen to (these elements don't change often). The _dynamic configuration_ contains everything that defines how the requests are handled by your system. -This configuration can change and is seamlessly hot-reloaded, without any request interuption or connection loss. +This configuration can change and is seamlessly hot-reloaded, without any request interruption or connection loss. !!! warning "Incompatible Configuration" Please be aware that the old configurations for Traefik v1.X are NOT compatible with the v2.X config as of now. diff --git a/docs/content/operations/cli.md b/docs/content/operations/cli.md index bef4a8510..4225943b5 100644 --- a/docs/content/operations/cli.md +++ b/docs/content/operations/cli.md @@ -26,7 +26,7 @@ traefik [--flag=flag_argument] [-f [flag_argument]] traefik [--flag[=true|false| ]] [-f [true|false| ]] ``` -### healthcheck +### `healthcheck` Calls Traefik `/ping` to check the health of Traefik. Its exit status is `0` if Traefik is healthy and `1` otherwise. @@ -50,12 +50,12 @@ $ traefik healthcheck OK: http://:8082/ping ``` -### version +### `version` Shows the current Traefik version. Usage: ```bash -traefik version [command] [flags] [arguments] +traefik version ``` diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index b6b09e3d9..3fc1daabc 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -37,7 +37,7 @@ Attach labels to your containers and let Traefik do the rest! Enabling the docker provider (Swarm Mode) ```toml - [docker] + [providers.docker] # swarm classic (1.12-) # endpoint = "tcp://127.0.0.1:2375" # docker swarm mode (1.12+) @@ -193,8 +193,8 @@ The container service name can be accessed as the `Name` identifier, and the template has access to all the labels defined on this container. ```toml tab="File" -[docker] -defaultRule = "" +[providers.docker] +defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" # ... ``` @@ -215,6 +215,48 @@ _Optional, Default=15_ Defines the polling interval (in seconds) in Swarm Mode. +### `constraints` + +_Optional, Default=""_ + +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. +That is to say, if none of the container's labels match the expression, no route for the container is created. +If the expression is empty, all detected containers are included. + +The expression syntax is based on the `Label("key", "value")`, and `LabelRegexp("key", "value")` functions, as well as the usual boolean logic, as shown in examples below. + +??? example "Constraints Expression Examples" + + ```toml + # Includes only containers having a label with key `a.label.name` and value `foo` + constraints = "Label(`a.label.name`, `foo`)" + ``` + + ```toml + # Excludes containers having any label with key `a.label.name` and value `foo` + constraints = "!Label(`a.label.name`, `value`)" + ``` + + ```toml + # With logical AND. + constraints = "Label(`a.label.name`, `valueA`) && Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical OR. + constraints = "Label(`a.label.name`, `valueA`) || Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical AND and OR, with precedence set by parentheses. + constraints = "Label(`a.label.name`, `valueA`) && (Label(`another.label.name`, `valueB`) || Label(`yet.another.label.name`, `valueC`))" + ``` + + ```toml + # Includes only containers having a label with key `a.label.name` and a value matching the `a.+` regular expression. + constraints = "LabelRegexp(`a.label.name`, `a.+`)" + ``` + ## Routing Configuration Options ### General @@ -286,10 +328,6 @@ You can tell Traefik to consider (or not) the container by setting `traefik.enab This option overrides the value of `exposedByDefault`. -#### `traefik.tags` - -Sets the tags for [constraints filtering](./overview.md#constraints-configuration). - #### `traefik.docker.network` Overrides the default docker network to use for connections to the container. diff --git a/docs/content/providers/marathon.md b/docs/content/providers/marathon.md index f8c0ff45a..178bb01ff 100644 --- a/docs/content/providers/marathon.md +++ b/docs/content/providers/marathon.md @@ -78,7 +78,7 @@ DCOSToken for DCOS environment. If set, it overrides the Authorization header. ```toml tab="File" -[marathon] +[providers.marathon] dcosToken = "xxxxxx" # ... ``` @@ -101,8 +101,8 @@ The app ID can be accessed as the Name identifier, and the template has access to all the labels defined on this Marathon application. ```toml tab="File" -[marathon] -defaultRule = "" +[providers.marathon] +defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" # ... ``` @@ -132,7 +132,7 @@ Marathon server endpoint. You can optionally specify multiple endpoints: ```toml tab="File" -[marathon] +[providers.marathon] endpoint = "http://10.241.1.71:8080,10.241.1.72:8080,10.241.1.73:8080" # ... ``` @@ -150,16 +150,59 @@ Exposes Marathon applications by default through Traefik. If set to false, applications that don't have a `traefik.enable=true` label will be ignored from the resulting routing configuration. -### `filterMarathonConstraints` +### `constraints` -_Optional, Default=false_ +_Optional, Default=""_ -Enables filtering using Marathon constraints. +Constraints is an expression that Traefik matches against the application's labels to determine whether to create any route for that application. +That is to say, if none of the application's labels match the expression, no route for the application is created. +In addition, the expression also matched against the application's constraints, such as described in [Marathon constraints](https://mesosphere.github.io/marathon/docs/constraints.html). +If the expression is empty, all detected applications are included. -If enabled, Traefik will take into account Marathon constraints, as defined in [Marathon constraints](https://mesosphere.github.io/marathon/docs/constraints.html). +The expression syntax is based on the `Label("key", "value")`, and `LabelRegexp("key", "value")`, as well as the usual boolean logic. +In addition, to match against marathon constraints, the function `MarathonConstraint("field:operator:value")` can be used, where the field, operator, and value parts are joined together in a single string with the `:` separator. -Each individual constraint will be treated as a verbatim compounded tag, -e.g. "rack_id:CLUSTER:rack-1", with all constraint groups concatenated together using ":". +??? example "Constraints Expression Examples" + + ```toml + # Includes only applications having a label with key `a.label.name` and value `foo` + constraints = "Label(`a.label.name`, `foo`)" + ``` + + ```toml + # Excludes applications having any label with key `a.label.name` and value `foo` + constraints = "!Label(`a.label.name`, `value`)" + ``` + + ```toml + # With logical AND. + constraints = "Label(`a.label.name`, `valueA`) && Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical OR. + constraints = "Label(`a.label.name`, `valueA`) || Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical AND and OR, with precedence set by parentheses. + constraints = "Label(`a.label.name`, `valueA`) && (Label(`another.label.name`, `valueB`) || Label(`yet.another.label.name`, `valueC`))" + ``` + + ```toml + # Includes only applications having a label with key `a.label.name` and a value matching the `a.+` regular expression. + constraints = "LabelRegexp(`a.label.name`, `a.+`)" + ``` + + ```toml + # Includes only applications having a Marathon constraint with field `A`, operator `B`, and value `C`. + constraints = "MarathonConstraint(`A:B:C`)" + ``` + + ```toml + # Uses both Marathon constraint and application label with logical operator. + constraints = "MarathonConstraint(`A:B:C`) && Label(`a.label.name`, `value`)" + ``` ### `forceTaskHostname` @@ -318,10 +361,6 @@ You can declare TCP Routers and/or Services using labels. Setting this option controls whether Traefik exposes the application. It overrides the value of `exposedByDefault`. -#### `traefik.tags` - -Sets the tags for [constraints filtering](./overview.md#constraints-configuration). - #### `traefik.marathon.ipadressidx` If a task has several IP addresses, this option specifies which one, in the list of available addresses, to select. diff --git a/docs/content/providers/overview.md b/docs/content/providers/overview.md index fc2c2ab7a..796d45d5c 100644 --- a/docs/content/providers/overview.md +++ b/docs/content/providers/overview.md @@ -26,74 +26,46 @@ Even if each provider is different, we can categorize them in four groups: Below is the list of the currently supported providers in Traefik. -| Provider | Type | Configuration Type | -|---------------------------------|--------------|--------------------| -| [Docker](./docker.md) | Orchestrator | Label | -| [File](./file.md) | Orchestrator | Custom Annotation | -| [Kubernetes](kubernetes-crd.md) | Orchestrator | Custom Resource | -| [Marathon](marathon.md) | Orchestrator | Label | +| Provider | Type | Configuration Type | +|-----------------------------------|--------------|--------------------| +| [Docker](./docker.md) | Orchestrator | Label | +| [Kubernetes](./kubernetes-crd.md) | Orchestrator | Custom Resource | +| [Marathon](./marathon.md) | Orchestrator | Label | +| [Rancher](./rancher.md) | Orchestrator | Label | +| [File](./file.md) | Manual | TOML format | !!! note "More Providers" - The current version of Traefik is in development and doesn't support (yet) every provider. See the previous version (1.7) for more providers. + The current version of Traefik is in development and doesn't support (yet) every provider. + See the previous version (1.7) for more providers. - -## Constraints Configuration +TODO (document TCP VS HTTP dynamic configuration) +--> -If you want to limit the scope of Traefik's service discovery, you can set constraints. -Doing so, Traefik will create routes for containers that match these constraints only. +## Restrict the Scope of Service Discovery -??? example "Containers with the api Tag" +By default Traefik will create routes for all detected containers. - ```toml - constraints = ["tag==api"] - ``` +If you want to limit the scope of Traefik's service discovery, +i.e. disallow route creation for some containers, +you can do so in two different ways: +either with the generic configuration option `exposedByDefault`, +or with a finer granularity mechanism based on constraints. -??? example "Containers without the api Tag" +### `exposedByDefault` and `traefik.enable` - ```toml - constraints = ["tag!=api"] - ``` - -??? example "Containers with tags starting with 'us-'" +List of providers that support that feature: - ```toml - constraints = ["tag==us-*"] - ``` +- [Docker](./docker.md#exposedbydefault) +- [Rancher](./rancher.md#exposedbydefault) +- [Marathon](./marathon.md#exposedbydefault) -??? example "Multiple constraints" +### Constraints - ```toml - # Multiple constraints - # - "tag==" must match with at least one tag - # - "tag!=" must match with none of tags - constraints = ["tag!=us-*", "tag!=asia-*"] - ``` +List of providers that support constraints: -??? note "List of Providers that Support Constraints" - - - Docker - - Consul K/V - - BoltDB - - Zookeeper - - ECS - - Etcd - - Consul Catalog - - Rancher - - Marathon - - Kubernetes (using a provider-specific mechanism based on label selectors) - -!!! note - - The constraint option belongs to the provider configuration itself. - - ??? example "Setting the Constraint Options for Docker" - - ```toml - [providers] - [providers.docker] - constraints = ["tag==api"] - ``` +- [Docker](./docker.md#constraints) +- [Rancher](./rancher.md#constraints) +- [Marathon](./marathon.md#constraints) +- [Kubernetes CRD](./kubernetes-crd.md#labelselector) diff --git a/docs/content/providers/rancher.md b/docs/content/providers/rancher.md index edc9de5e6..a9572d61b 100644 --- a/docs/content/providers/rancher.md +++ b/docs/content/providers/rancher.md @@ -19,64 +19,23 @@ Attach labels to your services and let Traefik do the rest! Enabling the rancher provider ```toml - [provider.rancher] + [Providers.Rancher] ``` Attaching labels to services ```yaml labels: - - traefik.http.services.my-service.rule=Host(my-domain) + - traefik.http.services.my-service.rule=Host(`my-domain`) ``` ## Provider Configuration Options -!!! tip "Browse the Reference" +??? tip "Browse the Reference" If you're in a hurry, maybe you'd rather go through the configuration reference: ```toml - ################################################################ - # Rancher Provider - ################################################################ - - # Enable Rancher Provider. - [rancher] - - # Expose Rancher services by default in Traefik. - # - # Optional - # - ExposedByDefault = "true" - - # Enable watch Rancher changes. - # - # Optional - # - watch = true - - # Filter services with unhealthy states and inactive states. - # - # Optional - # - EnableServiceHealthFilter = true - - # Defines the polling interval (in seconds). - # - # Optional - # - RefreshSeconds = true - - # Poll the Rancher metadata service for changes every `rancher.refreshSeconds`, which is less accurate - # - # Optional - # - IntervalPoll = false - - # Prefix used for accessing the Rancher metadata service - # - # Optional - # - Prefix = 15 + --8<-- "content/providers/rancher.toml" ``` ### `ExposedByDefault` @@ -99,8 +58,8 @@ The service name can be accessed as the `Name` identifier, and the template has access to all the labels defined on this container. ```toml tab="File" -[rancher] -defaultRule = "" +[Providers.Rancher] +defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" # ... ``` @@ -136,6 +95,50 @@ _Optional, Default=/latest_ Prefix used for accessing the Rancher metadata service +### `constraints` + +_Optional, Default=""_ + +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. +That is to say, if none of the container's labels match the expression, no route for the container is created. +If the expression is empty, all detected containers are included. + +The expression syntax is based on the `Label("key", "value")`, and `LabelRegexp("key", "value")` functions, as well as the usual boolean logic, as shown in examples below. + +??? example "Constraints Expression Examples" + + ```toml + # Includes only containers having a label with key `a.label.name` and value `foo` + constraints = "Label(`a.label.name`, `foo`)" + ``` + + ```toml + # Excludes containers having any label with key `a.label.name` and value `foo` + constraints = "!Label(`a.label.name`, `value`)" + ``` + + ```toml + # With logical AND. + constraints = "Label(`a.label.name`, `valueA`) && Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical OR. + constraints = "Label(`a.label.name`, `valueA`) || Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical AND and OR, with precedence set by parentheses. + constraints = "Label(`a.label.name`, `valueA`) && (Label(`another.label.name`, `valueB`) || Label(`yet.another.label.name`, `valueC`))" + ``` + + ```toml + # Includes only containers having a label with key `a.label.name` and a value matching the `a.+` regular expression. + constraints = "LabelRegexp(`a.label.name`, `a.+`)" + ``` + +## Routing Configuration Options + ### General Traefik creates, for each rancher service, a corresponding [service](../routing/services/index.md) and [router](../routing/routers/index.md). @@ -185,10 +188,6 @@ You can tell Traefik to consider (or not) the container by setting `traefik.enab This option overrides the value of `exposedByDefault`. -#### `traefik.tags` - -Sets the tags for [constraints filtering](./overview.md#constraints-configuration). - #### Port Lookup Traefik is now capable of detecting the port to use, by following the default rancher flow. diff --git a/docs/content/providers/rancher.toml b/docs/content/providers/rancher.toml new file mode 100644 index 000000000..5ce4bac50 --- /dev/null +++ b/docs/content/providers/rancher.toml @@ -0,0 +1,20 @@ +# Enable Rancher Provider. +[Providers.Rancher] + + # Expose Rancher services by default in Traefik. + ExposedByDefault = true + + # Enable watch Rancher changes. + Watch = true + + # Filter services with unhealthy states and inactive states. + EnableServiceHealthFilter = true + + # Defines the polling interval (in seconds). + RefreshSeconds = true + + # Poll the Rancher metadata service for changes every `rancher.refreshSeconds`, which is less accurate + IntervalPoll = false + + # Prefix used for accessing the Rancher metadata service + Prefix = "/latest" diff --git a/docs/content/reference/static-configuration/cli.txt b/docs/content/reference/static-configuration/cli.txt index 22f9c6326..900c809f8 100644 --- a/docs/content/reference/static-configuration/cli.txt +++ b/docs/content/reference/static-configuration/cli.txt @@ -249,17 +249,8 @@ Enable Docker backend with default settings. --providers.docker.constraints (Default: "") - Filter services by constraint, matching with Traefik tags. - ---providers.docker.constraints[n].key (Default: "") - The provider label that will be matched against. In practice, it is always - 'tag'. - ---providers.docker.constraints[n].mustmatch (Default: "false") - Whether the matching operator is equals or not equals. - ---providers.docker.constraints[n].value (Default: "") - The value that will be matched against. + Constraints is an expression that Traefik matches against the container's labels + to determine whether to create any route for that container. --providers.docker.defaultrule (Default: "Host(`{{ normalize .Name }}`)") Default rule. @@ -382,17 +373,8 @@ Basic authentication Password. --providers.marathon.constraints (Default: "") - Filter services by constraint, matching with Traefik tags. - ---providers.marathon.constraints[n].key (Default: "") - The provider label that will be matched against. In practice, it is always - 'tag'. - ---providers.marathon.constraints[n].mustmatch (Default: "false") - Whether the matching operator is equals or not equals. - ---providers.marathon.constraints[n].value (Default: "") - The value that will be matched against. + Constraints is an expression that Traefik matches against the application's + labels to determine whether to create any route for that application. --providers.marathon.dcostoken (Default: "") DCOSToken for DCOS environment, This will override the Authorization header. @@ -409,9 +391,6 @@ --providers.marathon.exposedbydefault (Default: "true") Expose Marathon apps by default. ---providers.marathon.filtermarathonconstraints (Default: "false") - Enable use of Marathon constraints in constraint filtering. - --providers.marathon.forcetaskhostname (Default: "false") Force to use the task's hostname. @@ -457,17 +436,8 @@ Enable Rancher backend with default settings. --providers.rancher.constraints (Default: "") - Filter services by constraint, matching with Traefik tags. - ---providers.rancher.constraints[n].key (Default: "") - The provider label that will be matched against. In practice, it is always - 'tag'. - ---providers.rancher.constraints[n].mustmatch (Default: "false") - Whether the matching operator is equals or not equals. - ---providers.rancher.constraints[n].value (Default: "") - The value that will be matched against. + Constraints is an expression that Traefik matches against the container's labels + to determine whether to create any route for that container. --providers.rancher.defaultrule (Default: "Host(`{{ normalize .Name }}`)") Default rule. diff --git a/docs/content/reference/static-configuration/env.md b/docs/content/reference/static-configuration/env.md index 1b2d4075f..da791adb3 100644 --- a/docs/content/reference/static-configuration/env.md +++ b/docs/content/reference/static-configuration/env.md @@ -241,16 +241,7 @@ Middleware list. Enable Docker backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS`: -Filter services by constraint, matching with Traefik tags. - -`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS[n]_KEY`: -The provider label that will be matched against. In practice, it is always 'tag'. - -`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS[n]_MUSTMATCH`: -Whether the matching operator is equals or not equals. (Default: ```false```) - -`TRAEFIK_PROVIDERS_DOCKER_CONSTRAINTS[n]_VALUE`: -The value that will be matched against. +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. `TRAEFIK_PROVIDERS_DOCKER_DEFAULTRULE`: Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) @@ -373,16 +364,7 @@ Basic authentication User. Basic authentication Password. `TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS`: -Filter services by constraint, matching with Traefik tags. - -`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS[n]_KEY`: -The provider label that will be matched against. In practice, it is always 'tag'. - -`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS[n]_MUSTMATCH`: -Whether the matching operator is equals or not equals. (Default: ```false```) - -`TRAEFIK_PROVIDERS_MARATHON_CONSTRAINTS[n]_VALUE`: -The value that will be matched against. +Constraints is an expression that Traefik matches against the application's labels to determine whether to create any route for that application. `TRAEFIK_PROVIDERS_MARATHON_DCOSTOKEN`: DCOSToken for DCOS environment, This will override the Authorization header. @@ -399,9 +381,6 @@ Marathon server endpoint. You can also specify multiple endpoint for Marathon. ( `TRAEFIK_PROVIDERS_MARATHON_EXPOSEDBYDEFAULT`: Expose Marathon apps by default. (Default: ```true```) -`TRAEFIK_PROVIDERS_MARATHON_FILTERMARATHONCONSTRAINTS`: -Enable use of Marathon constraints in constraint filtering. (Default: ```false```) - `TRAEFIK_PROVIDERS_MARATHON_FORCETASKHOSTNAME`: Force to use the task's hostname. (Default: ```false```) @@ -445,16 +424,7 @@ Backends throttle duration: minimum duration between 2 events from providers bef Enable Rancher backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS`: -Filter services by constraint, matching with Traefik tags. - -`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS[n]_KEY`: -The provider label that will be matched against. In practice, it is always 'tag'. - -`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS[n]_MUSTMATCH`: -Whether the matching operator is equals or not equals. (Default: ```false```) - -`TRAEFIK_PROVIDERS_RANCHER_CONSTRAINTS[n]_VALUE`: -The value that will be matched against. +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. `TRAEFIK_PROVIDERS_RANCHER_DEFAULTRULE`: Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 05c269221..da26229c5 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -41,16 +41,7 @@ SwarmMode = true Network = "foobar" SwarmModeRefreshSeconds = 42 - - [[Providers.Docker.Constraints]] - Key = "foobar" - MustMatch = true - Regex = "foobar" - - [[Providers.Docker.Constraints]] - Key = "foobar" - MustMatch = true - Regex = "foobar" + Constraints = "foobar" [Providers.Docker.TLS] CA = "foobar" @@ -73,23 +64,13 @@ DefaultRule = "foobar" ExposedByDefault = true DCOSToken = "foobar" - FilterMarathonConstraints = true DialerTimeout = 42 ResponseHeaderTimeout = 42 TLSHandshakeTimeout = 42 KeepAlive = 42 ForceTaskHostname = true RespectReadinessChecks = true - - [[Providers.Marathon.Constraints]] - Key = "foobar" - MustMatch = true - Regex = "foobar" - - [[Providers.Marathon.Constraints]] - Key = "foobar" - MustMatch = true - Regex = "foobar" + Constraints = "foobar" [Providers.Marathon.TLS] CA = "foobar" @@ -134,16 +115,7 @@ RefreshSeconds = 42 IntervalPoll = true Prefix = "foobar" - - [[Providers.Rancher.Constraints]] - Key = "foobar" - MustMatch = true - Regex = "foobar" - - [[Providers.Rancher.Constraints]] - Key = "foobar" - MustMatch = true - Regex = "foobar" + Constraints = "foobar" [API] EntryPoint = "foobar" diff --git a/pkg/anonymize/anonymize_config_test.go b/pkg/anonymize/anonymize_config_test.go index ebf08d390..5dc291eda 100644 --- a/pkg/anonymize/anonymize_config_test.go +++ b/pkg/anonymize/anonymize_config_test.go @@ -7,7 +7,6 @@ import ( "github.com/containous/traefik/pkg/config/static" "github.com/containous/traefik/pkg/ping" - "github.com/containous/traefik/pkg/provider" "github.com/containous/traefik/pkg/provider/acme" acmeprovider "github.com/containous/traefik/pkg/provider/acme" "github.com/containous/traefik/pkg/provider/docker" @@ -153,20 +152,7 @@ func TestDo_globalConfiguration(t *testing.T) { } config.Providers.Docker = &docker.Provider{ - Constrainer: provider.Constrainer{ - Constraints: []*types.Constraint{ - { - Key: "file Constraints Key 1", - Value: "file Constraints Regex 2", - MustMatch: true, - }, - { - Key: "file Constraints Key 1", - Value: "file Constraints Regex 2", - MustMatch: true, - }, - }, - }, + Constraints: `Label("foo", "bar")`, Watch: true, Endpoint: "MyEndPoint", DefaultRule: "PathPrefix(`/`)", diff --git a/pkg/config/file/file_node_test.go b/pkg/config/file/file_node_test.go index cb8217d12..66be88999 100644 --- a/pkg/config/file/file_node_test.go +++ b/pkg/config/file/file_node_test.go @@ -248,7 +248,6 @@ func Test_decodeFileToNode_Toml(t *testing.T) { {Name: "DialerTimeout", Value: "42"}, {Name: "Endpoint", Value: "foobar"}, {Name: "ExposedByDefault", Value: "true"}, - {Name: "FilterMarathonConstraints", Value: "true"}, {Name: "ForceTaskHostname", Value: "true"}, {Name: "KeepAlive", Value: "42"}, {Name: "RespectReadinessChecks", Value: "true"}, @@ -518,7 +517,6 @@ func Test_decodeFileToNode_Yaml(t *testing.T) { {Name: "DialerTimeout", Value: "42"}, {Name: "Endpoint", Value: "foobar"}, {Name: "ExposedByDefault", Value: "true"}, - {Name: "FilterMarathonConstraints", Value: "true"}, {Name: "ForceTaskHostname", Value: "true"}, {Name: "KeepAlive", Value: "42"}, {Name: "RespectReadinessChecks", Value: "true"}, diff --git a/pkg/config/file/fixtures/sample.toml b/pkg/config/file/fixtures/sample.toml index a3a6373ba..04d8bc9c5 100644 --- a/pkg/config/file/fixtures/sample.toml +++ b/pkg/config/file/fixtures/sample.toml @@ -74,7 +74,6 @@ DefaultRule = "foobar" ExposedByDefault = true DCOSToken = "foobar" - FilterMarathonConstraints = true DialerTimeout = 42 ResponseHeaderTimeout = 42 TLSHandshakeTimeout = 42 diff --git a/pkg/config/file/fixtures/sample.yml b/pkg/config/file/fixtures/sample.yml index adaf8c02b..d5908ad38 100644 --- a/pkg/config/file/fixtures/sample.yml +++ b/pkg/config/file/fixtures/sample.yml @@ -69,7 +69,6 @@ Providers: DefaultRule: foobar ExposedByDefault: true DCOSToken: foobar - FilterMarathonConstraints: true DialerTimeout: 42 ResponseHeaderTimeout: 42 TLSHandshakeTimeout: 42 diff --git a/pkg/provider/constrainer.go b/pkg/provider/constrainer.go deleted file mode 100644 index 2afe1abcd..000000000 --- a/pkg/provider/constrainer.go +++ /dev/null @@ -1,27 +0,0 @@ -package provider - -import "github.com/containous/traefik/pkg/types" - -// Constrainer Filter services by constraint, matching with Traefik tags. -type Constrainer struct { - Constraints []*types.Constraint `description:"Filter services by constraint, matching with Traefik tags." export:"true"` -} - -// MatchConstraints must match with EVERY single constraint -// returns first constraint that do not match or nil. -func (c *Constrainer) MatchConstraints(tags []string) (bool, *types.Constraint) { - // if there is no tags and no constraints, filtering is disabled - if len(tags) == 0 && len(c.Constraints) == 0 { - return true, nil - } - - for _, constraint := range c.Constraints { - // xor: if ok and constraint.MustMatch are equal, then no tag is currently matching with the constraint - if ok := constraint.MatchConstraintWithAtLeastOneTag(tags); ok != constraint.MustMatch { - return false, constraint - } - } - - // If no constraint or every constraints matching - return true, nil -} diff --git a/pkg/provider/constraints/constraints.go b/pkg/provider/constraints/constraints.go new file mode 100644 index 000000000..5094442b3 --- /dev/null +++ b/pkg/provider/constraints/constraints.go @@ -0,0 +1,100 @@ +package constraints + +import ( + "errors" + "regexp" + "strings" + + "github.com/vulcand/predicate" +) + +// MarathonConstraintPrefix is the prefix for each label's key created from a Marathon application constraint. +// It is used in order to create a specific and unique pattern for these labels. +const MarathonConstraintPrefix = "Traefik-Marathon-505F9E15-BDC7-45E7-828D-C06C7BAB8091" + +type constraintFunc func(map[string]string) bool + +// Match reports whether the expression matches with the given labels. +// The expression must match any logical boolean combination of: +// - `Label(labelName, labelValue)` +// - `LabelRegex(labelName, regexValue)` +// - `MarathonConstraint(field:operator:value)` +func Match(labels map[string]string, expr string) (bool, error) { + if expr == "" { + return true, nil + } + + p, err := predicate.NewParser(predicate.Def{ + Operators: predicate.Operators{ + AND: andFunc, + NOT: notFunc, + OR: orFunc, + }, + Functions: map[string]interface{}{ + "Label": labelFn, + "LabelRegex": labelRegexFn, + "MarathonConstraint": marathonFn, + }, + }) + if err != nil { + return false, err + } + + parse, err := p.Parse(expr) + if err != nil { + return false, err + } + + fn, ok := parse.(constraintFunc) + if !ok { + return false, errors.New("not a constraintFunc") + } + return fn(labels), nil +} + +func labelFn(name, value string) constraintFunc { + return func(labels map[string]string) bool { + return labels[name] == value + } +} + +func labelRegexFn(name, expr string) constraintFunc { + return func(labels map[string]string) bool { + matched, err := regexp.MatchString(expr, labels[name]) + if err != nil { + return false + } + return matched + } +} + +func marathonFn(value string) constraintFunc { + return func(labels map[string]string) bool { + for k, v := range labels { + if strings.HasPrefix(k, MarathonConstraintPrefix) { + if v == value { + return true + } + } + } + return false + } +} + +func andFunc(a, b constraintFunc) constraintFunc { + return func(labels map[string]string) bool { + return a(labels) && b(labels) + } +} + +func orFunc(a, b constraintFunc) constraintFunc { + return func(labels map[string]string) bool { + return a(labels) || b(labels) + } +} + +func notFunc(a constraintFunc) constraintFunc { + return func(labels map[string]string) bool { + return !a(labels) + } +} diff --git a/pkg/provider/constraints/constraints_test.go b/pkg/provider/constraints/constraints_test.go new file mode 100644 index 000000000..c6198a329 --- /dev/null +++ b/pkg/provider/constraints/constraints_test.go @@ -0,0 +1,204 @@ +package constraints + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatch(t *testing.T) { + testCases := []struct { + expr string + labels map[string]string + expected bool + expectedErr bool + }{ + { + expr: `Label("hello", "world")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: true, + }, + { + expr: `Label("hello", "worlds")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `Label("hi", "world")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `!Label("hello", "world")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `Label("hello", "world") && Label("foo", "bar")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: true, + }, + { + expr: `Label("hello", "worlds") && Label("foo", "bar")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `Label("hello", "world") && !Label("foo", "bar")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `Label("hello", "world") || Label("foo", "bar")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: true, + }, + { + expr: `Label("hello", "worlds") || Label("foo", "bar")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: true, + }, + { + expr: `Label("hello", "world") || !Label("foo", "bar")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: true, + }, + { + expr: `Label("hello")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expectedErr: true, + }, + { + expr: `Foo("hello")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expectedErr: true, + }, + { + expr: `Label("hello", "bar")`, + expected: false, + }, + { + expr: ``, + expected: true, + }, + { + expr: `MarathonConstraint("bar")`, + labels: map[string]string{ + "hello": "world", + MarathonConstraintPrefix + "-1": "bar", + MarathonConstraintPrefix + "-2": "foo", + }, + expected: true, + }, + { + expr: `MarathonConstraint("bur")`, + labels: map[string]string{ + "hello": "world", + MarathonConstraintPrefix + "-1": "bar", + MarathonConstraintPrefix + "-2": "foo", + }, + expected: false, + }, + { + expr: `Label("hello", "world") && MarathonConstraint("bar")`, + labels: map[string]string{ + "hello": "world", + MarathonConstraintPrefix + "-1": "bar", + MarathonConstraintPrefix + "-2": "foo", + }, + expected: true, + }, + { + expr: `LabelRegex("hello", "w\\w+")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: true, + }, + { + expr: `LabelRegex("hello", "w\\w+s")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `LabelRegex("hi", "w\\w+")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `!LabelRegex("hello", "w\\w+")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + { + expr: `LabelRegex("hello", "w(\\w+")`, + labels: map[string]string{ + "hello": "world", + "foo": "bar", + }, + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.expr, func(t *testing.T) { + t.Parallel() + + matches, err := Match(test.labels, test.expr) + if test.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, test.expected, matches) + }) + } +} diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index c8cf95d3f..ccb293820 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -11,6 +11,7 @@ import ( "github.com/containous/traefik/pkg/config/label" "github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/provider" + "github.com/containous/traefik/pkg/provider/constraints" "github.com/docker/go-connections/nat" ) @@ -123,10 +124,13 @@ func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool return false } - if ok, failingConstraint := p.MatchConstraints(container.ExtraConf.Tags); !ok { - if failingConstraint != nil { - logger.Debugf("Container pruned by %q constraint", failingConstraint.String()) - } + matches, err := constraints.Match(container.Labels, p.Constraints) + if err != nil { + logger.Error("Error matching constraints expression: %v", err) + return false + } + if !matches { + logger.Debugf("Container pruned by constraint expression: %q", p.Constraints) return false } diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index ac801157d..f7b8d94fb 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/containous/traefik/pkg/config" - "github.com/containous/traefik/pkg/types" docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/go-connections/nat" @@ -339,7 +338,7 @@ func Test_buildConfiguration(t *testing.T) { testCases := []struct { desc string containers []dockerData - constraints []*types.Constraint + constraints string expected *config.Configuration }{ { @@ -1924,13 +1923,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "bar", - }, - }, + constraints: `Label("traefik.tags", "bar")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, @@ -1965,13 +1958,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, }, - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "foo", - }, - }, + constraints: `Label("traefik.tags", "foo")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, diff --git a/pkg/provider/docker/docker.go b/pkg/provider/docker/docker.go index 09bea10ce..3e4507647 100644 --- a/pkg/provider/docker/docker.go +++ b/pkg/provider/docker/docker.go @@ -45,7 +45,7 @@ var _ provider.Provider = (*Provider)(nil) // Provider holds configurations of the provider. type Provider struct { - provider.Constrainer `description:"List of constraints used to filter out some containers." export:"true"` + Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." export:"true"` Watch bool `description:"Watch provider." export:"true"` Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint."` DefaultRule string `description:"Default rule."` diff --git a/pkg/provider/docker/label.go b/pkg/provider/docker/label.go index 99d8fb349..53705c0a7 100644 --- a/pkg/provider/docker/label.go +++ b/pkg/provider/docker/label.go @@ -14,7 +14,6 @@ const ( // configuration Contains information from the labels that are globals (not related to the dynamic configuration) or specific to the provider. type configuration struct { Enable bool - Tags []string Docker specificConfiguration } @@ -31,7 +30,7 @@ func (p *Provider) getConfiguration(container dockerData) (configuration, error) }, } - err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.enable", "traefik.tags") + err := label.Decode(container.Labels, &conf, "traefik.docker.", "traefik.enable") if err != nil { return configuration{}, err } diff --git a/pkg/provider/marathon/config.go b/pkg/provider/marathon/config.go index 166775bad..84f98b256 100644 --- a/pkg/provider/marathon/config.go +++ b/pkg/provider/marathon/config.go @@ -13,6 +13,7 @@ import ( "github.com/containous/traefik/pkg/config/label" "github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/provider" + "github.com/containous/traefik/pkg/provider/constraints" "github.com/gambol99/go-marathon" ) @@ -29,11 +30,20 @@ func (p *Provider) buildConfiguration(ctx context.Context, applications *maratho continue } - if !p.keepApplication(ctxApp, extraConf) { + labels := stringValueMap(app.Labels) + + if app.Constraints != nil { + for i, constraintParts := range *app.Constraints { + key := constraints.MarathonConstraintPrefix + "-" + strconv.Itoa(i) + labels[key] = strings.Join(constraintParts, ":") + } + } + + if !p.keepApplication(ctxApp, extraConf, labels) { continue } - confFromLabel, err := label.DecodeConfiguration(stringValueMap(app.Labels)) + confFromLabel, err := label.DecodeConfiguration(labels) if err != nil { logger.Error(err) continue @@ -65,7 +75,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, applications *maratho Labels map[string]string }{ Name: app.ID, - Labels: stringValueMap(app.Labels), + Labels: labels, } serviceName := getServiceName(app) @@ -164,7 +174,7 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, app maratho return nil } -func (p *Provider) keepApplication(ctx context.Context, extraConf configuration) bool { +func (p *Provider) keepApplication(ctx context.Context, extraConf configuration, labels map[string]string) bool { logger := log.FromContext(ctx) // Filter disabled application. @@ -174,10 +184,13 @@ func (p *Provider) keepApplication(ctx context.Context, extraConf configuration) } // Filter by constraints. - if ok, failingConstraint := p.MatchConstraints(extraConf.Tags); !ok { - if failingConstraint != nil { - logger.Debugf("Filtering Marathon application, pruned by %q constraint", failingConstraint.String()) - } + matches, err := constraints.Match(labels, p.Constraints) + if err != nil { + logger.Error("Error matching constraints expression: %v", err) + return false + } + if !matches { + logger.Debugf("Marathon application filtered by constraint expression: %q", p.Constraints) return false } diff --git a/pkg/provider/marathon/config_test.go b/pkg/provider/marathon/config_test.go index 4e8529de8..34adeaf4c 100644 --- a/pkg/provider/marathon/config_test.go +++ b/pkg/provider/marathon/config_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/containous/traefik/pkg/config" - "github.com/containous/traefik/pkg/types" "github.com/gambol99/go-marathon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,12 +28,11 @@ func TestGetConfigurationAPIErrors(t *testing.T) { func TestBuildConfiguration(t *testing.T) { testCases := []struct { - desc string - applications *marathon.Applications - constraints []*types.Constraint - filterMarathonConstraints bool - defaultRule string - expected *config.Configuration + desc string + applications *marathon.Applications + constraints string + defaultRule string + expected *config.Configuration }{ { desc: "simple application", @@ -1065,13 +1063,7 @@ func TestBuildConfiguration(t *testing.T) { withTasks(localhostTask(taskPorts(80, 81))), withLabel("traefik.tags", "foo"), )), - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "bar", - }, - }, + constraints: `Label("traefik.tags", "bar")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, @@ -1093,14 +1085,7 @@ func TestBuildConfiguration(t *testing.T) { withTasks(localhostTask(taskPorts(80, 81))), constraint("rack_id:CLUSTER:rack-1"), )), - filterMarathonConstraints: true, - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "rack_id:CLUSTER:rack-2", - }, - }, + constraints: `MarathonConstraint("rack_id:CLUSTER:rack-2")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, @@ -1122,14 +1107,7 @@ func TestBuildConfiguration(t *testing.T) { withTasks(localhostTask(taskPorts(80, 81))), constraint("rack_id:CLUSTER:rack-1"), )), - filterMarathonConstraints: true, - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "rack_id:CLUSTER:rack-1", - }, - }, + constraints: `MarathonConstraint("rack_id:CLUSTER:rack-1")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, @@ -1167,14 +1145,7 @@ func TestBuildConfiguration(t *testing.T) { withTasks(localhostTask(taskPorts(80, 81))), withLabel("traefik.tags", "bar"), )), - - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "bar", - }, - }, + constraints: `Label("traefik.tags", "bar")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, @@ -1417,9 +1388,8 @@ func TestBuildConfiguration(t *testing.T) { } p := &Provider{ - DefaultRule: defaultRule, - ExposedByDefault: true, - FilterMarathonConstraints: test.filterMarathonConstraints, + DefaultRule: defaultRule, + ExposedByDefault: true, } p.Constraints = test.constraints @@ -1473,7 +1443,7 @@ func TestApplicationFilterEnabled(t *testing.T) { extraConf, err := provider.getConfiguration(app) require.NoError(t, err) - if provider.keepApplication(context.Background(), extraConf) != test.expected { + if provider.keepApplication(context.Background(), extraConf, stringValueMap(app.Labels)) != test.expected { t.Errorf("got unexpected filtering = %t", !test.expected) } }) diff --git a/pkg/provider/marathon/label.go b/pkg/provider/marathon/label.go index 42a8a9bbe..3dba526db 100644 --- a/pkg/provider/marathon/label.go +++ b/pkg/provider/marathon/label.go @@ -2,7 +2,6 @@ package marathon import ( "math" - "strings" "github.com/containous/traefik/pkg/config/label" "github.com/gambol99/go-marathon" @@ -10,7 +9,6 @@ import ( type configuration struct { Enable bool - Tags []string Marathon specificConfiguration } @@ -23,23 +21,16 @@ func (p *Provider) getConfiguration(app marathon.Application) (configuration, er conf := configuration{ Enable: p.ExposedByDefault, - Tags: nil, Marathon: specificConfiguration{ IPAddressIdx: math.MinInt32, }, } - err := label.Decode(labels, &conf, "traefik.marathon.", "traefik.enable", "traefik.tags") + err := label.Decode(labels, &conf, "traefik.marathon.", "traefik.enable") if err != nil { return configuration{}, err } - if p.FilterMarathonConstraints && app.Constraints != nil { - for _, constraintParts := range *app.Constraints { - conf.Tags = append(conf.Tags, strings.Join(constraintParts, ":")) - } - } - return conf, nil } diff --git a/pkg/provider/marathon/label_test.go b/pkg/provider/marathon/label_test.go index 7cb011939..472b91d23 100644 --- a/pkg/provider/marathon/label_test.go +++ b/pkg/provider/marathon/label_test.go @@ -23,12 +23,10 @@ func TestGetConfiguration(t *testing.T) { Labels: &map[string]string{}, }, p: Provider{ - ExposedByDefault: false, - FilterMarathonConstraints: false, + ExposedByDefault: false, }, expected: configuration{ Enable: false, - Tags: nil, Marathon: specificConfiguration{ IPAddressIdx: math.MinInt32, }, @@ -43,12 +41,10 @@ func TestGetConfiguration(t *testing.T) { }, }, p: Provider{ - ExposedByDefault: false, - FilterMarathonConstraints: false, + ExposedByDefault: false, }, expected: configuration{ Enable: true, - Tags: nil, Marathon: specificConfiguration{ IPAddressIdx: math.MinInt32, }, @@ -63,12 +59,10 @@ func TestGetConfiguration(t *testing.T) { }, }, p: Provider{ - ExposedByDefault: false, - FilterMarathonConstraints: false, + ExposedByDefault: false, }, expected: configuration{ Enable: false, - Tags: nil, Marathon: specificConfiguration{ IPAddressIdx: 4, }, @@ -83,14 +77,10 @@ func TestGetConfiguration(t *testing.T) { Labels: &map[string]string{}, }, p: Provider{ - ExposedByDefault: false, - FilterMarathonConstraints: true, + ExposedByDefault: false, }, expected: configuration{ Enable: false, - Tags: []string{ - "key:value", - }, Marathon: specificConfiguration{ IPAddressIdx: math.MinInt32, }, @@ -103,12 +93,10 @@ func TestGetConfiguration(t *testing.T) { Labels: &map[string]string{}, }, p: Provider{ - ExposedByDefault: true, - FilterMarathonConstraints: false, + ExposedByDefault: true, }, expected: configuration{ Enable: true, - Tags: nil, Marathon: specificConfiguration{ IPAddressIdx: math.MinInt32, }, @@ -123,32 +111,10 @@ func TestGetConfiguration(t *testing.T) { }, }, p: Provider{ - ExposedByDefault: true, - FilterMarathonConstraints: false, + ExposedByDefault: true, }, expected: configuration{ Enable: false, - Tags: nil, - Marathon: specificConfiguration{ - IPAddressIdx: math.MinInt32, - }, - }, - }, - { - desc: "Tags in label", - app: marathon.Application{ - Constraints: &[][]string{}, - Labels: &map[string]string{ - "traefik.tags": "mytags", - }, - }, - p: Provider{ - ExposedByDefault: true, - FilterMarathonConstraints: false, - }, - expected: configuration{ - Enable: true, - Tags: []string{"mytags"}, Marathon: specificConfiguration{ IPAddressIdx: math.MinInt32, }, diff --git a/pkg/provider/marathon/marathon.go b/pkg/provider/marathon/marathon.go index 3c7112c7d..5fb64ccd7 100644 --- a/pkg/provider/marathon/marathon.go +++ b/pkg/provider/marathon/marathon.go @@ -45,26 +45,24 @@ var _ provider.Provider = (*Provider)(nil) // Provider holds configuration of the provider. type Provider struct { - provider.Constrainer `description:"List of constraints used to filter out some containers." export:"true"` - - Trace bool `description:"Display additional provider logs." export:"true"` - Watch bool `description:"Watch provider." export:"true"` - Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon." export:"true"` - DefaultRule string `description:"Default rule."` - ExposedByDefault bool `description:"Expose Marathon apps by default." export:"true"` - DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header." export:"true"` - FilterMarathonConstraints bool `description:"Enable use of Marathon constraints in constraint filtering." export:"true"` - TLS *types.ClientTLS `description:"Enable TLS support." export:"true"` - DialerTimeout types.Duration `description:"Set a dialer timeout for Marathon." export:"true"` - ResponseHeaderTimeout types.Duration `description:"Set a response header timeout for Marathon." export:"true"` - TLSHandshakeTimeout types.Duration `description:"Set a TLS handshake timeout for Marathon." export:"true"` - KeepAlive types.Duration `description:"Set a TCP Keep Alive time." export:"true"` - ForceTaskHostname bool `description:"Force to use the task's hostname." export:"true"` - Basic *Basic `description:"Enable basic authentication." export:"true"` - RespectReadinessChecks bool `description:"Filter out tasks with non-successful readiness checks during deployments." export:"true"` - readyChecker *readinessChecker - marathonClient marathon.Marathon - defaultRuleTpl *template.Template + Constraints string `description:"Constraints is an expression that Traefik matches against the application's labels to determine whether to create any route for that application." export:"true"` + Trace bool `description:"Display additional provider logs." export:"true"` + Watch bool `description:"Watch provider." export:"true"` + Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon." export:"true"` + DefaultRule string `description:"Default rule."` + ExposedByDefault bool `description:"Expose Marathon apps by default." export:"true"` + DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header." export:"true"` + TLS *types.ClientTLS `description:"Enable TLS support." export:"true"` + DialerTimeout types.Duration `description:"Set a dialer timeout for Marathon." export:"true"` + ResponseHeaderTimeout types.Duration `description:"Set a response header timeout for Marathon." export:"true"` + TLSHandshakeTimeout types.Duration `description:"Set a TLS handshake timeout for Marathon." export:"true"` + KeepAlive types.Duration `description:"Set a TCP Keep Alive time." export:"true"` + ForceTaskHostname bool `description:"Force to use the task's hostname." export:"true"` + Basic *Basic `description:"Enable basic authentication." export:"true"` + RespectReadinessChecks bool `description:"Filter out tasks with non-successful readiness checks during deployments." export:"true"` + readyChecker *readinessChecker + marathonClient marathon.Marathon + defaultRuleTpl *template.Template } // SetDefaults sets the default values. diff --git a/pkg/provider/rancher/config.go b/pkg/provider/rancher/config.go index e6bf6a044..3eeae9a37 100644 --- a/pkg/provider/rancher/config.go +++ b/pkg/provider/rancher/config.go @@ -11,6 +11,7 @@ import ( "github.com/containous/traefik/pkg/config/label" "github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/provider" + "github.com/containous/traefik/pkg/provider/constraints" ) func (p *Provider) buildConfiguration(ctx context.Context, services []rancherData) *config.Configuration { @@ -120,10 +121,13 @@ func (p *Provider) keepService(ctx context.Context, service rancherData) bool { return false } - if ok, failingConstraint := p.MatchConstraints(service.ExtraConf.Tags); !ok { - if failingConstraint != nil { - logger.Debugf("service pruned by %q constraint", failingConstraint.String()) - } + matches, err := constraints.Match(service.Labels, p.Constraints) + if err != nil { + logger.Error("Error matching constraints expression: %v", err) + return false + } + if !matches { + logger.Debugf("Service pruned by constraint expression: %q", p.Constraints) return false } diff --git a/pkg/provider/rancher/config_test.go b/pkg/provider/rancher/config_test.go index 077eb42a2..0adff55f1 100644 --- a/pkg/provider/rancher/config_test.go +++ b/pkg/provider/rancher/config_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/containous/traefik/pkg/config" - "github.com/containous/traefik/pkg/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,7 +13,7 @@ func Test_buildConfiguration(t *testing.T) { testCases := []struct { desc string containers []rancherData - constraints []*types.Constraint + constraints string expected *config.Configuration }{ { @@ -330,13 +329,7 @@ func Test_buildConfiguration(t *testing.T) { State: "", }, }, - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "bar", - }, - }, + constraints: `Label("traefik.tags", "bar")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, @@ -363,13 +356,7 @@ func Test_buildConfiguration(t *testing.T) { State: "", }, }, - constraints: []*types.Constraint{ - { - Key: "tag", - MustMatch: true, - Value: "foo", - }, - }, + constraints: `Label("traefik.tags", "foo")`, expected: &config.Configuration{ TCP: &config.TCPConfiguration{ Routers: map[string]*config.TCPRouter{}, diff --git a/pkg/provider/rancher/label.go b/pkg/provider/rancher/label.go index 64c36842f..bd42ff51c 100644 --- a/pkg/provider/rancher/label.go +++ b/pkg/provider/rancher/label.go @@ -6,7 +6,6 @@ import ( type configuration struct { Enable bool - Tags []string } func (p *Provider) getConfiguration(service rancherData) (configuration, error) { @@ -14,7 +13,7 @@ func (p *Provider) getConfiguration(service rancherData) (configuration, error) Enable: p.ExposedByDefault, } - err := label.Decode(service.Labels, &conf, "traefik.rancher.", "traefik.enable", "traefik.tags") + err := label.Decode(service.Labels, &conf, "traefik.rancher.", "traefik.enable") if err != nil { return configuration{}, err } diff --git a/pkg/provider/rancher/rancher.go b/pkg/provider/rancher/rancher.go index 1d120af36..76a3f9028 100644 --- a/pkg/provider/rancher/rancher.go +++ b/pkg/provider/rancher/rancher.go @@ -40,8 +40,7 @@ var _ provider.Provider = (*Provider)(nil) // Provider holds configurations of the provider. type Provider struct { - provider.Constrainer `description:"List of constraints used to filter out some containers." export:"true"` - + Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." export:"true"` Watch bool `description:"Watch provider." export:"true"` DefaultRule string `description:"Default rule."` ExposedByDefault bool `description:"Expose containers by default." export:"true"` diff --git a/pkg/provider/rest/rest.go b/pkg/provider/rest/rest.go index 60db12d8b..02aa10c5f 100644 --- a/pkg/provider/rest/rest.go +++ b/pkg/provider/rest/rest.go @@ -25,7 +25,6 @@ type Provider struct { // SetDefaults sets the default values. func (p *Provider) SetDefaults() { p.EntryPoint = "traefik" - // FIXME p.EntryPoint = static.DefaultInternalEntryPointName } var templatesRenderer = render.New(render.Options{Directory: "nowhere"}) diff --git a/pkg/types/constraints.go b/pkg/types/constraints.go deleted file mode 100644 index 2328e795e..000000000 --- a/pkg/types/constraints.go +++ /dev/null @@ -1,88 +0,0 @@ -package types - -import ( - "encoding" - "errors" - "fmt" - "strings" - - "github.com/ryanuber/go-glob" -) - -// Constraint holds a parsed constraint expression. -// FIXME replace by a string. -type Constraint struct { - Key string `description:"The provider label that will be matched against. In practice, it is always 'tag'." export:"true"` - // MustMatch is true if operator is "==" or false if operator is "!=" - MustMatch bool `description:"Whether the matching operator is equals or not equals." export:"true"` - Value string `description:"The value that will be matched against." export:"true"` // TODO: support regex -} - -// NewConstraint receives a string and return a *Constraint, after checking syntax and parsing the constraint expression. -func NewConstraint(exp string) (*Constraint, error) { - sep := "" - constraint := &Constraint{} - - switch { - case strings.Contains(exp, "=="): - sep = "==" - constraint.MustMatch = true - case strings.Contains(exp, "!="): - sep = "!=" - constraint.MustMatch = false - default: - 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.Value = kv[1] - return constraint, nil - } - - return nil, fmt.Errorf("incorrect constraint expression: %s", exp) -} - -func (c *Constraint) String() string { - if c.MustMatch { - return c.Key + "==" + c.Value - } - return c.Key + "!=" + c.Value -} - -var _ encoding.TextUnmarshaler = (*Constraint)(nil) - -// UnmarshalText defines how unmarshal in TOML parsing -func (c *Constraint) UnmarshalText(text []byte) error { - constraint, err := NewConstraint(string(text)) - if err != nil { - return err - } - c.Key = constraint.Key - c.MustMatch = constraint.MustMatch - c.Value = constraint.Value - return nil -} - -var _ encoding.TextMarshaler = (*Constraint)(nil) - -// MarshalText encodes the receiver into UTF-8-encoded text and returns the result. -func (c *Constraint) MarshalText() (text []byte, err error) { - return []byte(c.String()), nil -} - -// MatchConstraintWithAtLeastOneTag tests a constraint for one single service. -func (c *Constraint) MatchConstraintWithAtLeastOneTag(tags []string) bool { - for _, tag := range tags { - if glob.Glob(c.Value, tag) { - return true - } - } - return false -} diff --git a/pkg/types/metrics.go b/pkg/types/metrics.go index 736f4d306..9a1a763b2 100644 --- a/pkg/types/metrics.go +++ b/pkg/types/metrics.go @@ -23,7 +23,6 @@ type Prometheus struct { func (p *Prometheus) SetDefaults() { p.Buckets = []float64{0.1, 0.3, 1.2, 5} p.EntryPoint = "traefik" - // FIXME p.EntryPoint = static.DefaultInternalEntryPointName } // Datadog contains address and metrics pushing interval configuration diff --git a/vendor/github.com/ryanuber/go-glob/LICENSE b/vendor/github.com/ryanuber/go-glob/LICENSE deleted file mode 100644 index bdfbd9514..000000000 --- a/vendor/github.com/ryanuber/go-glob/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Ryan Uber - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/ryanuber/go-glob/glob.go b/vendor/github.com/ryanuber/go-glob/glob.go deleted file mode 100644 index e67db3be1..000000000 --- a/vendor/github.com/ryanuber/go-glob/glob.go +++ /dev/null @@ -1,56 +0,0 @@ -package glob - -import "strings" - -// The character which is treated like a glob -const GLOB = "*" - -// Glob will test a string pattern, potentially containing globs, against a -// subject string. The result is a simple true/false, determining whether or -// not the glob pattern matched the subject text. -func Glob(pattern, subj string) bool { - // Empty pattern can only match empty subject - if pattern == "" { - return subj == pattern - } - - // If the pattern _is_ a glob, it matches everything - if pattern == GLOB { - return true - } - - parts := strings.Split(pattern, GLOB) - - if len(parts) == 1 { - // No globs in pattern, so test for equality - return subj == pattern - } - - leadingGlob := strings.HasPrefix(pattern, GLOB) - trailingGlob := strings.HasSuffix(pattern, GLOB) - end := len(parts) - 1 - - // Go over the leading parts and ensure they match. - for i := 0; i < end; i++ { - idx := strings.Index(subj, parts[i]) - - switch i { - case 0: - // Check the first section. Requires special handling. - if !leadingGlob && idx != 0 { - return false - } - default: - // Check that the middle parts match. - if idx < 0 { - return false - } - } - - // Trim evaluated text from subj as we loop over the pattern. - subj = subj[idx+len(parts[i]):] - } - - // Reached the last section. Requires special handling. - return trailingGlob || strings.HasSuffix(subj, parts[end]) -} diff --git a/vendor/github.com/vulcand/predicate/lib.go b/vendor/github.com/vulcand/predicate/lib.go index 013e16c50..c313c1bae 100644 --- a/vendor/github.com/vulcand/predicate/lib.go +++ b/vendor/github.com/vulcand/predicate/lib.go @@ -119,6 +119,14 @@ func Or(a, b BoolPredicate) BoolPredicate { } } +// Not is a boolean predicate that calls a boolean predicate +// and returns negated result +func Not(a BoolPredicate) BoolPredicate { + return func() bool { + return !a() + } +} + // GetFieldByTag returns a field from the object based on the tag func GetFieldByTag(ival interface{}, tagName string, fieldNames []string) (interface{}, error) { if len(fieldNames) == 0 { diff --git a/vendor/github.com/vulcand/predicate/parse.go b/vendor/github.com/vulcand/predicate/parse.go index 03518cf16..b80e7361a 100644 --- a/vendor/github.com/vulcand/predicate/parse.go +++ b/vendor/github.com/vulcand/predicate/parse.go @@ -59,6 +59,16 @@ func (p *predicateParser) parseNode(node ast.Node) (interface{}, error) { return callFunction(fn, arguments) case *ast.ParenExpr: return p.parseNode(n.X) + case *ast.UnaryExpr: + joinFn, err := p.getJoinFunction(n.Op) + if err != nil { + return nil, err + } + node, err := p.parseNode(n.X) + if err != nil { + return nil, err + } + return callFunction(joinFn, []interface{}{node}) } return nil, trace.BadParameter("unsupported %T", node) } @@ -122,6 +132,20 @@ func (p *predicateParser) evaluateExpr(n ast.Expr) (interface{}, error) { return nil, trace.Wrap(err) } return val, nil + case *ast.CallExpr: + name, err := getIdentifier(l.Fun) + if err != nil { + return nil, err + } + fn, err := p.getFunction(name) + if err != nil { + return nil, err + } + arguments, err := p.evaluateArguments(l.Args) + if err != nil { + return nil, err + } + return callFunction(fn, arguments) default: return nil, trace.BadParameter("%T is not supported", n) } @@ -161,6 +185,8 @@ func (p *predicateParser) joinPredicates(op token.Token, a, b interface{}) (inte func (p *predicateParser) getJoinFunction(op token.Token) (interface{}, error) { var fn interface{} switch op { + case token.NOT: + fn = p.d.Operators.NOT case token.LAND: fn = p.d.Operators.AND case token.LOR: diff --git a/vendor/github.com/vulcand/predicate/predicate.go b/vendor/github.com/vulcand/predicate/predicate.go index 256e70146..c5aacb488 100644 --- a/vendor/github.com/vulcand/predicate/predicate.go +++ b/vendor/github.com/vulcand/predicate/predicate.go @@ -1,3 +1,20 @@ +/* +Copyright 2014-2018 Vulcand Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + /* Predicate package used to create interpreted mini languages with Go syntax - mostly to define various predicates for configuration, e.g. Latency() > 40 || ErrorRate() > 0.5. @@ -76,6 +93,7 @@ type Operators struct { OR interface{} AND interface{} + NOT interface{} } // Parser takes the string with expression and calls the operators and functions.