From 9b9f4be6a4471cc9d8bedee37406892aad13aae9 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 28 Nov 2019 21:56:04 +0100 Subject: [PATCH] Add KV store providers (dynamic configuration only) Co-authored-by: Jean-Baptiste Doumenjou --- docs/content/providers/consul.md | 216 +++++ docs/content/providers/etcd.md | 216 +++++ docs/content/providers/redis.md | 216 +++++ docs/content/providers/zookeeper.md | 216 +++++ .../reference/dynamic-configuration/kv-ref.md | 246 +++++ .../reference/dynamic-configuration/kv.md | 8 + .../reference/static-configuration/cli-ref.md | 120 +++ .../reference/static-configuration/env-ref.md | 120 +++ .../reference/static-configuration/file.toml | 44 + .../reference/static-configuration/file.yaml | 52 + docs/content/routing/providers/kv.md | 392 ++++++++ docs/mkdocs.yml | 6 + go.sum | 16 + integration/consul_test.go | 159 ++++ integration/etcd_test.go | 159 ++++ integration/fixtures/consul/simple.toml | 16 + integration/fixtures/etcd/simple.toml | 16 + integration/fixtures/redis/simple.toml | 16 + integration/fixtures/zookeeper/simple.toml | 16 + integration/integration_test.go | 4 + integration/redis_test.go | 159 ++++ integration/resources/compose/consul.yml | 4 + integration/resources/compose/etcd.yml | 5 + integration/resources/compose/redis.yml | 4 + integration/resources/compose/zookeeper.yml | 4 + integration/testdata/rawdata-consul.json | 219 +++++ integration/testdata/rawdata-etcd.json | 219 +++++ integration/testdata/rawdata-redis.json | 219 +++++ integration/testdata/rawdata-zk.json | 219 +++++ integration/zk_test.go | 159 ++++ internal/gendoc.go | 96 ++ pkg/config/env/env.go | 9 +- pkg/config/file/file.go | 5 +- pkg/config/flag/flag.go | 9 +- pkg/config/kv/kv.go | 75 ++ pkg/config/kv/kv_node.go | 128 +++ pkg/config/kv/kv_node_test.go | 274 ++++++ pkg/config/kv/kv_test.go | 63 ++ pkg/config/parser/element_fill.go | 56 +- pkg/config/parser/element_fill_test.go | 2 +- pkg/config/parser/element_nodes.go | 23 +- pkg/config/parser/element_nodes_test.go | 3 +- pkg/config/parser/flat_encode.go | 3 +- pkg/config/parser/flat_encode_test.go | 3 +- pkg/config/parser/nodes_metadata.go | 47 +- pkg/config/parser/nodes_metadata_test.go | 2 +- pkg/config/parser/parser.go | 8 +- pkg/config/static/static_config.go | 28 +- pkg/provider/aggregator/aggregator.go | 16 + pkg/provider/kv/consul/consul.go | 25 + pkg/provider/kv/etcd/etcd.go | 25 + pkg/provider/kv/kv.go | 191 ++++ pkg/provider/kv/kv_mock_test.go | 122 +++ pkg/provider/kv/kv_test.go | 892 ++++++++++++++++++ pkg/provider/kv/redis/redis.go | 25 + pkg/provider/kv/storewrapper.go | 118 +++ pkg/provider/kv/zk/zk.go | 25 + webui/src/statics/providers/consul.svg | 6 + webui/src/statics/providers/etcd.svg | 6 + webui/src/statics/providers/redis.svg | 17 + webui/src/statics/providers/zookeeper.svg | 128 +++ 61 files changed, 5825 insertions(+), 70 deletions(-) create mode 100644 docs/content/providers/consul.md create mode 100644 docs/content/providers/etcd.md create mode 100644 docs/content/providers/redis.md create mode 100644 docs/content/providers/zookeeper.md create mode 100644 docs/content/reference/dynamic-configuration/kv-ref.md create mode 100644 docs/content/reference/dynamic-configuration/kv.md create mode 100644 docs/content/routing/providers/kv.md create mode 100644 integration/consul_test.go create mode 100644 integration/etcd_test.go create mode 100644 integration/fixtures/consul/simple.toml create mode 100644 integration/fixtures/etcd/simple.toml create mode 100644 integration/fixtures/redis/simple.toml create mode 100644 integration/fixtures/zookeeper/simple.toml create mode 100644 integration/redis_test.go create mode 100644 integration/resources/compose/consul.yml create mode 100644 integration/resources/compose/etcd.yml create mode 100644 integration/resources/compose/redis.yml create mode 100644 integration/resources/compose/zookeeper.yml create mode 100644 integration/testdata/rawdata-consul.json create mode 100644 integration/testdata/rawdata-etcd.json create mode 100644 integration/testdata/rawdata-redis.json create mode 100644 integration/testdata/rawdata-zk.json create mode 100644 integration/zk_test.go create mode 100644 pkg/config/kv/kv.go create mode 100644 pkg/config/kv/kv_node.go create mode 100644 pkg/config/kv/kv_node_test.go create mode 100644 pkg/config/kv/kv_test.go create mode 100644 pkg/provider/kv/consul/consul.go create mode 100644 pkg/provider/kv/etcd/etcd.go create mode 100644 pkg/provider/kv/kv.go create mode 100644 pkg/provider/kv/kv_mock_test.go create mode 100644 pkg/provider/kv/kv_test.go create mode 100644 pkg/provider/kv/redis/redis.go create mode 100644 pkg/provider/kv/storewrapper.go create mode 100644 pkg/provider/kv/zk/zk.go create mode 100644 webui/src/statics/providers/consul.svg create mode 100644 webui/src/statics/providers/etcd.svg create mode 100644 webui/src/statics/providers/redis.svg create mode 100644 webui/src/statics/providers/zookeeper.svg diff --git a/docs/content/providers/consul.md b/docs/content/providers/consul.md new file mode 100644 index 000000000..0556f838e --- /dev/null +++ b/docs/content/providers/consul.md @@ -0,0 +1,216 @@ +# Traefik & Consul + +A Story of KV store & Containers +{: .subtitle } + +Store your configuration in Consul and let Traefik do the rest! + +## Routing Configuration + +See the dedicated section in [routing](../routing/providers/kv.md). + +## Provider Configuration + +### `endpoints` + +_Required, Default="127.0.0.1:8500"_ + +Defines how to access to Consul. + +```toml tab="File (TOML)" +[providers.consul] + endpoints = ["127.0.0.1:8500"] +``` + +```yaml tab="File (YAML)" +providers: + consul: + endpoints: + - "127.0.0.1:8500" +``` + +```bash tab="CLI" +--providers.consul.endpoints=127.0.0.1:8500 +``` + +### `rootKey` + +Defines the root key of the configuration. + +_Required, Default="traefik"_ + +```toml tab="File (TOML)" +[providers.consul] + rootKey = "traefik" +``` + +```yaml tab="File (YAML)" +providers: + consul: + rootKey: "traefik" +``` + +```bash tab="CLI" +--providers.consul.rootkey=traefik +``` + +### `username` + +Defines a username to connect with Consul. + +_Optional, Default=""_ + +```toml tab="File (TOML)" +[providers.consul] + # ... + username = "foo" +``` + +```yaml tab="File (YAML)" +providers: + consul: + # ... + usename: "foo" +``` + +```bash tab="CLI" +--providers.consul.username=foo +``` + +### `password` + +_Optional, Default=""_ + +Defines a password to connect with Consul. + +```toml tab="File (TOML)" +[providers.consul] + # ... + password = "bar" +``` + +```yaml tab="File (YAML)" +providers: + consul: + # ... + password: "bar" +``` + +```bash tab="CLI" +--providers.consul.password=foo +``` + +### `tls` + +_Optional_ + +#### `tls.ca` + +Certificate Authority used for the secured connection to Consul. + +```toml tab="File (TOML)" +[providers.consul.tls] + ca = "path/to/ca.crt" +``` + +```yaml tab="File (YAML)" +providers: + consul: + tls: + ca: path/to/ca.crt +``` + +```bash tab="CLI" +--providers.consul.tls.ca=path/to/ca.crt +``` + +#### `tls.caOptional` + +Policy followed for the secured connection with TLS Client Authentication to Consul. +Requires `tls.ca` to be defined. + +- `true`: VerifyClientCertIfGiven +- `false`: RequireAndVerifyClientCert +- if `tls.ca` is undefined NoClientCert + +```toml tab="File (TOML)" +[providers.consul.tls] + caOptional = true +``` + +```yaml tab="File (YAML)" +providers: + consul: + tls: + caOptional: true +``` + +```bash tab="CLI" +--providers.consul.tls.caOptional=true +``` + +#### `tls.cert` + +Public certificate used for the secured connection to Consul. + +```toml tab="File (TOML)" +[providers.consul.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + consul: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.consul.tls.cert=path/to/foo.cert +--providers.consul.tls.key=path/to/foo.key +``` + +#### `tls.key` + +Private certificate used for the secured connection to Consul. + +```toml tab="File (TOML)" +[providers.consul.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + consul: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.consul.tls.cert=path/to/foo.cert +--providers.consul.tls.key=path/to/foo.key +``` + +#### `tls.insecureSkipVerify` + +If `insecureSkipVerify` is `true`, TLS for the connection to Consul accepts any certificate presented by the server and any host name in that certificate. + +```toml tab="File (TOML)" +[providers.consul.tls] + insecureSkipVerify = true +``` + +```yaml tab="File (YAML)" +providers: + consul: + tls: + insecureSkipVerify: true +``` + +```bash tab="CLI" +--providers.consul.tls.insecureSkipVerify=true +``` diff --git a/docs/content/providers/etcd.md b/docs/content/providers/etcd.md new file mode 100644 index 000000000..ddbb46a90 --- /dev/null +++ b/docs/content/providers/etcd.md @@ -0,0 +1,216 @@ +# Traefik & Etcd + +A Story of KV store & Containers +{: .subtitle } + +Store your configuration in Etcd and let Traefik do the rest! + +## Routing Configuration + +See the dedicated section in [routing](../routing/providers/kv.md). + +## Provider Configuration + +### `endpoints` + +_Required, Default="127.0.0.1:2379"_ + +Defines how to access to Etcd. + +```toml tab="File (TOML)" +[providers.etcd] + endpoints = ["127.0.0.1:2379"] +``` + +```yaml tab="File (YAML)" +providers: + etcd: + endpoints: + - "127.0.0.1:2379" +``` + +```bash tab="CLI" +--providers.etcd.endpoints=127.0.0.1:2379 +``` + +### `rootKey` + +Defines the root key of the configuration. + +_Required, Default="traefik"_ + +```toml tab="File (TOML)" +[providers.etcd] + rootKey = "traefik" +``` + +```yaml tab="File (YAML)" +providers: + etcd: + rootKey: "traefik" +``` + +```bash tab="CLI" +--providers.etcd.rootkey=traefik +``` + +### `username` + +Defines a username to connect with Etcd. + +_Optional, Default=""_ + +```toml tab="File (TOML)" +[providers.etcd] + # ... + username = "foo" +``` + +```yaml tab="File (YAML)" +providers: + etcd: + # ... + usename: "foo" +``` + +```bash tab="CLI" +--providers.etcd.username=foo +``` + +### `password` + +_Optional, Default=""_ + +Defines a password to connect with Etcd. + +```toml tab="File (TOML)" +[providers.etcd] + # ... + password = "bar" +``` + +```yaml tab="File (YAML)" +providers: + etcd: + # ... + password: "bar" +``` + +```bash tab="CLI" +--providers.etcd.password=foo +``` + +### `tls` + +_Optional_ + +#### `tls.ca` + +Certificate Authority used for the secured connection to Etcd. + +```toml tab="File (TOML)" +[providers.etcd.tls] + ca = "path/to/ca.crt" +``` + +```yaml tab="File (YAML)" +providers: + etcd: + tls: + ca: path/to/ca.crt +``` + +```bash tab="CLI" +--providers.etcd.tls.ca=path/to/ca.crt +``` + +#### `tls.caOptional` + +Policy followed for the secured connection with TLS Client Authentication to Etcd. +Requires `tls.ca` to be defined. + +- `true`: VerifyClientCertIfGiven +- `false`: RequireAndVerifyClientCert +- if `tls.ca` is undefined NoClientCert + +```toml tab="File (TOML)" +[providers.etcd.tls] + caOptional = true +``` + +```yaml tab="File (YAML)" +providers: + etcd: + tls: + caOptional: true +``` + +```bash tab="CLI" +--providers.etcd.tls.caOptional=true +``` + +#### `tls.cert` + +Public certificate used for the secured connection to Etcd. + +```toml tab="File (TOML)" +[providers.etcd.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + etcd: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.etcd.tls.cert=path/to/foo.cert +--providers.etcd.tls.key=path/to/foo.key +``` + +#### `tls.key` + +Private certificate used for the secured connection to Etcd. + +```toml tab="File (TOML)" +[providers.etcd.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + etcd: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.etcd.tls.cert=path/to/foo.cert +--providers.etcd.tls.key=path/to/foo.key +``` + +#### `tls.insecureSkipVerify` + +If `insecureSkipVerify` is `true`, TLS for the connection to Etcd accepts any certificate presented by the server and any host name in that certificate. + +```toml tab="File (TOML)" +[providers.etcd.tls] + insecureSkipVerify = true +``` + +```yaml tab="File (YAML)" +providers: + etcd: + tls: + insecureSkipVerify: true +``` + +```bash tab="CLI" +--providers.etcd.tls.insecureSkipVerify=true +``` diff --git a/docs/content/providers/redis.md b/docs/content/providers/redis.md new file mode 100644 index 000000000..c48b79730 --- /dev/null +++ b/docs/content/providers/redis.md @@ -0,0 +1,216 @@ +# Traefik & Redis + +A Story of KV store & Containers +{: .subtitle } + +Store your configuration in Redis and let Traefik do the rest! + +## Routing Configuration + +See the dedicated section in [routing](../routing/providers/kv.md). + +## Provider Configuration + +### `endpoints` + +_Required, Default="127.0.0.1:6379"_ + +Defines how to access to Redis. + +```toml tab="File (TOML)" +[providers.redis] + endpoints = ["127.0.0.1:6379"] +``` + +```yaml tab="File (YAML)" +providers: + redis: + endpoints: + - "127.0.0.1:6379" +``` + +```bash tab="CLI" +--providers.redis.endpoints=127.0.0.1:6379 +``` + +### `rootKey` + +Defines the root key of the configuration. + +_Required, Default="traefik"_ + +```toml tab="File (TOML)" +[providers.redis] + rootKey = "traefik" +``` + +```yaml tab="File (YAML)" +providers: + redis: + rootKey: "traefik" +``` + +```bash tab="CLI" +--providers.redis.rootkey=traefik +``` + +### `username` + +Defines a username to connect with Redis. + +_Optional, Default=""_ + +```toml tab="File (TOML)" +[providers.redis] + # ... + username = "foo" +``` + +```yaml tab="File (YAML)" +providers: + redis: + # ... + usename: "foo" +``` + +```bash tab="CLI" +--providers.redis.username=foo +``` + +### `password` + +_Optional, Default=""_ + +Defines a password to connect with Redis. + +```toml tab="File (TOML)" +[providers.redis] + # ... + password = "bar" +``` + +```yaml tab="File (YAML)" +providers: + redis: + # ... + password: "bar" +``` + +```bash tab="CLI" +--providers.redis.password=foo +``` + +### `tls` + +_Optional_ + +#### `tls.ca` + +Certificate Authority used for the secured connection to Redis. + +```toml tab="File (TOML)" +[providers.redis.tls] + ca = "path/to/ca.crt" +``` + +```yaml tab="File (YAML)" +providers: + redis: + tls: + ca: path/to/ca.crt +``` + +```bash tab="CLI" +--providers.redis.tls.ca=path/to/ca.crt +``` + +#### `tls.caOptional` + +Policy followed for the secured connection with TLS Client Authentication to Redis. +Requires `tls.ca` to be defined. + +- `true`: VerifyClientCertIfGiven +- `false`: RequireAndVerifyClientCert +- if `tls.ca` is undefined NoClientCert + +```toml tab="File (TOML)" +[providers.redis.tls] + caOptional = true +``` + +```yaml tab="File (YAML)" +providers: + redis: + tls: + caOptional: true +``` + +```bash tab="CLI" +--providers.redis.tls.caOptional=true +``` + +#### `tls.cert` + +Public certificate used for the secured connection to Redis. + +```toml tab="File (TOML)" +[providers.redis.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + redis: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.redis.tls.cert=path/to/foo.cert +--providers.redis.tls.key=path/to/foo.key +``` + +#### `tls.key` + +Private certificate used for the secured connection to Redis. + +```toml tab="File (TOML)" +[providers.redis.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + redis: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.redis.tls.cert=path/to/foo.cert +--providers.redis.tls.key=path/to/foo.key +``` + +#### `tls.insecureSkipVerify` + +If `insecureSkipVerify` is `true`, TLS for the connection to Redis accepts any certificate presented by the server and any host name in that certificate. + +```toml tab="File (TOML)" +[providers.redis.tls] + insecureSkipVerify = true +``` + +```yaml tab="File (YAML)" +providers: + redis: + tls: + insecureSkipVerify: true +``` + +```bash tab="CLI" +--providers.redis.tls.insecureSkipVerify=true +``` diff --git a/docs/content/providers/zookeeper.md b/docs/content/providers/zookeeper.md new file mode 100644 index 000000000..79f9ea1f0 --- /dev/null +++ b/docs/content/providers/zookeeper.md @@ -0,0 +1,216 @@ +# Traefik & ZooKeeper + +A Story of KV store & Containers +{: .subtitle } + +Store your configuration in ZooKeeper and let Traefik do the rest! + +## Routing Configuration + +See the dedicated section in [routing](../routing/providers/kv.md). + +## Provider Configuration + +### `endpoints` + +_Required, Default="127.0.0.1:2181"_ + +Defines how to access to ZooKeeper. + +```toml tab="File (TOML)" +[providers.zooKeeper] + endpoints = ["127.0.0.1:2181"] +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + endpoints: + - "127.0.0.1:2181" +``` + +```bash tab="CLI" +--providers.zookeeper.endpoints=127.0.0.1:2181 +``` + +### `rootKey` + +Defines the root key of the configuration. + +_Required, Default="traefik"_ + +```toml tab="File (TOML)" +[providers.zooKeeper] + rootKey = "traefik" +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + rootKey: "traefik" +``` + +```bash tab="CLI" +--providers.zookeeper.rootkey=traefik +``` + +### `username` + +Defines a username to connect with ZooKeeper. + +_Optional, Default=""_ + +```toml tab="File (TOML)" +[providers.zooKeeper] + # ... + username = "foo" +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + # ... + usename: "foo" +``` + +```bash tab="CLI" +--providers.zookeeper.username=foo +``` + +### `password` + +_Optional, Default=""_ + +Defines a password to connect with ZooKeeper. + +```toml tab="File (TOML)" +[providers.zooKeeper] + # ... + password = "bar" +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + # ... + password: "bar" +``` + +```bash tab="CLI" +--providers.zookeeper.password=foo +``` + +### `tls` + +_Optional_ + +#### `tls.ca` + +Certificate Authority used for the secured connection to ZooKeeper. + +```toml tab="File (TOML)" +[providers.zooKeeper.tls] + ca = "path/to/ca.crt" +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + tls: + ca: path/to/ca.crt +``` + +```bash tab="CLI" +--providers.zookeeper.tls.ca=path/to/ca.crt +``` + +#### `tls.caOptional` + +Policy followed for the secured connection with TLS Client Authentication to ZooKeeper. +Requires `tls.ca` to be defined. + +- `true`: VerifyClientCertIfGiven +- `false`: RequireAndVerifyClientCert +- if `tls.ca` is undefined NoClientCert + +```toml tab="File (TOML)" +[providers.zooKeeper.tls] + caOptional = true +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + tls: + caOptional: true +``` + +```bash tab="CLI" +--providers.zookeeper.tls.caOptional=true +``` + +#### `tls.cert` + +Public certificate used for the secured connection to ZooKeeper. + +```toml tab="File (TOML)" +[providers.zooKeeper.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.zookeeper.tls.cert=path/to/foo.cert +--providers.zookeeper.tls.key=path/to/foo.key +``` + +#### `tls.key` + +Private certificate used for the secured connection to ZooKeeper. + +```toml tab="File (TOML)" +[providers.zooKeeper.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```bash tab="CLI" +--providers.zookeeper.tls.cert=path/to/foo.cert +--providers.zookeeper.tls.key=path/to/foo.key +``` + +#### `tls.insecureSkipVerify` + +If `insecureSkipVerify` is `true`, TLS for the connection to ZooKeeper accepts any certificate presented by the server and any host name in that certificate. + +```toml tab="File (TOML)" +[providers.zooKeeper.tls] + insecureSkipVerify = true +``` + +```yaml tab="File (YAML)" +providers: + zooKeeper: + tls: + insecureSkipVerify: true +``` + +```bash tab="CLI" +--providers.zookeeper.tls.insecureSkipVerify=true +``` diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md new file mode 100644 index 000000000..5fae2e8a3 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -0,0 +1,246 @@ +| `traefik/http/middlewares/Middleware00/addPrefix/prefix` | `foobar` | +| `traefik/http/middlewares/Middleware01/basicAuth/headerField` | `foobar` | +| `traefik/http/middlewares/Middleware01/basicAuth/realm` | `foobar` | +| `traefik/http/middlewares/Middleware01/basicAuth/removeHeader` | `true` | +| `traefik/http/middlewares/Middleware01/basicAuth/users/0` | `foobar` | +| `traefik/http/middlewares/Middleware01/basicAuth/users/1` | `foobar` | +| `traefik/http/middlewares/Middleware01/basicAuth/usersFile` | `foobar` | +| `traefik/http/middlewares/Middleware02/buffering/maxRequestBodyBytes` | `42` | +| `traefik/http/middlewares/Middleware02/buffering/maxResponseBodyBytes` | `42` | +| `traefik/http/middlewares/Middleware02/buffering/memRequestBodyBytes` | `42` | +| `traefik/http/middlewares/Middleware02/buffering/memResponseBodyBytes` | `42` | +| `traefik/http/middlewares/Middleware02/buffering/retryExpression` | `foobar` | +| `traefik/http/middlewares/Middleware03/chain/middlewares/0` | `foobar` | +| `traefik/http/middlewares/Middleware03/chain/middlewares/1` | `foobar` | +| `traefik/http/middlewares/Middleware04/circuitBreaker/expression` | `foobar` | +| `traefik/http/middlewares/Middleware05/compress` | `` | +| `traefik/http/middlewares/Middleware06/digestAuth/headerField` | `foobar` | +| `traefik/http/middlewares/Middleware06/digestAuth/realm` | `foobar` | +| `traefik/http/middlewares/Middleware06/digestAuth/removeHeader` | `true` | +| `traefik/http/middlewares/Middleware06/digestAuth/users/0` | `foobar` | +| `traefik/http/middlewares/Middleware06/digestAuth/users/1` | `foobar` | +| `traefik/http/middlewares/Middleware06/digestAuth/usersFile` | `foobar` | +| `traefik/http/middlewares/Middleware07/errors/query` | `foobar` | +| `traefik/http/middlewares/Middleware07/errors/service` | `foobar` | +| `traefik/http/middlewares/Middleware07/errors/status/0` | `foobar` | +| `traefik/http/middlewares/Middleware07/errors/status/1` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/address` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/authResponseHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/authResponseHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/tls/ca` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/tls/caOptional` | `true` | +| `traefik/http/middlewares/Middleware08/forwardAuth/tls/cert` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/tls/insecureSkipVerify` | `true` | +| `traefik/http/middlewares/Middleware08/forwardAuth/tls/key` | `foobar` | +| `traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader` | `true` | +| `traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials` | `true` | +| `traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlAllowMethods/0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlAllowMethods/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlAllowOrigin` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/accessControlMaxAge` | `42` | +| `traefik/http/middlewares/Middleware09/headers/addVaryHeader` | `true` | +| `traefik/http/middlewares/Middleware09/headers/allowedHosts/0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/allowedHosts/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/browserXssFilter` | `true` | +| `traefik/http/middlewares/Middleware09/headers/contentSecurityPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/contentTypeNosniff` | `true` | +| `traefik/http/middlewares/Middleware09/headers/customBrowserXSSValue` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/customFrameOptionsValue` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/customResponseHeaders/name0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/customResponseHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/featurePolicy` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/forceSTSHeader` | `true` | +| `traefik/http/middlewares/Middleware09/headers/frameDeny` | `true` | +| `traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/isDevelopment` | `true` | +| `traefik/http/middlewares/Middleware09/headers/publicKey` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/referrerPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/sslForceHost` | `true` | +| `traefik/http/middlewares/Middleware09/headers/sslHost` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name0` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware09/headers/sslRedirect` | `true` | +| `traefik/http/middlewares/Middleware09/headers/sslTemporaryRedirect` | `true` | +| `traefik/http/middlewares/Middleware09/headers/stsIncludeSubdomains` | `true` | +| `traefik/http/middlewares/Middleware09/headers/stsPreload` | `true` | +| `traefik/http/middlewares/Middleware09/headers/stsSeconds` | `42` | +| `traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/depth` | `42` | +| `traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware10/ipWhiteList/sourceRange/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/ipWhiteList/sourceRange/1` | `foobar` | +| `traefik/http/middlewares/Middleware11/inFlightReq/amount` | `42` | +| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/depth` | `42` | +| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` | +| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/requestHeaderName` | `foobar` | +| `traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/requestHost` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/commonName` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/country` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/domainComponent` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/locality` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/organization` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/province` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/serialNumber` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/notAfter` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/notBefore` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/sans` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/commonName` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/country` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/domainComponent` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/locality` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/organization` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/province` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/serialNumber` | `true` | +| `traefik/http/middlewares/Middleware12/passTLSClientCert/pem` | `true` | +| `traefik/http/middlewares/Middleware13/rateLimit/average` | `42` | +| `traefik/http/middlewares/Middleware13/rateLimit/burst` | `42` | +| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/depth` | `42` | +| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` | +| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/requestHeaderName` | `foobar` | +| `traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/requestHost` | `true` | +| `traefik/http/middlewares/Middleware14/redirectRegex/permanent` | `true` | +| `traefik/http/middlewares/Middleware14/redirectRegex/regex` | `foobar` | +| `traefik/http/middlewares/Middleware14/redirectRegex/replacement` | `foobar` | +| `traefik/http/middlewares/Middleware15/redirectScheme/permanent` | `true` | +| `traefik/http/middlewares/Middleware15/redirectScheme/port` | `foobar` | +| `traefik/http/middlewares/Middleware15/redirectScheme/scheme` | `foobar` | +| `traefik/http/middlewares/Middleware16/replacePath/path` | `foobar` | +| `traefik/http/middlewares/Middleware17/replacePathRegex/regex` | `foobar` | +| `traefik/http/middlewares/Middleware17/replacePathRegex/replacement` | `foobar` | +| `traefik/http/middlewares/Middleware18/retry/attempts` | `42` | +| `traefik/http/middlewares/Middleware19/stripPrefix/forceSlash` | `true` | +| `traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0` | `foobar` | +| `traefik/http/middlewares/Middleware19/stripPrefix/prefixes/1` | `foobar` | +| `traefik/http/middlewares/Middleware20/stripPrefixRegex/regex/0` | `foobar` | +| `traefik/http/middlewares/Middleware20/stripPrefixRegex/regex/1` | `foobar` | +| `traefik/http/routers/Router0/entryPoints/0` | `foobar` | +| `traefik/http/routers/Router0/entryPoints/1` | `foobar` | +| `traefik/http/routers/Router0/middlewares/0` | `foobar` | +| `traefik/http/routers/Router0/middlewares/1` | `foobar` | +| `traefik/http/routers/Router0/priority` | `42` | +| `traefik/http/routers/Router0/rule` | `foobar` | +| `traefik/http/routers/Router0/service` | `foobar` | +| `traefik/http/routers/Router0/tls/certResolver` | `foobar` | +| `traefik/http/routers/Router0/tls/domains/0/main` | `foobar` | +| `traefik/http/routers/Router0/tls/domains/0/sans/0` | `foobar` | +| `traefik/http/routers/Router0/tls/domains/0/sans/1` | `foobar` | +| `traefik/http/routers/Router0/tls/domains/1/main` | `foobar` | +| `traefik/http/routers/Router0/tls/domains/1/sans/0` | `foobar` | +| `traefik/http/routers/Router0/tls/domains/1/sans/1` | `foobar` | +| `traefik/http/routers/Router0/tls/options` | `foobar` | +| `traefik/http/routers/Router1/entryPoints/0` | `foobar` | +| `traefik/http/routers/Router1/entryPoints/1` | `foobar` | +| `traefik/http/routers/Router1/middlewares/0` | `foobar` | +| `traefik/http/routers/Router1/middlewares/1` | `foobar` | +| `traefik/http/routers/Router1/priority` | `42` | +| `traefik/http/routers/Router1/rule` | `foobar` | +| `traefik/http/routers/Router1/service` | `foobar` | +| `traefik/http/routers/Router1/tls/certResolver` | `foobar` | +| `traefik/http/routers/Router1/tls/domains/0/main` | `foobar` | +| `traefik/http/routers/Router1/tls/domains/0/sans/0` | `foobar` | +| `traefik/http/routers/Router1/tls/domains/0/sans/1` | `foobar` | +| `traefik/http/routers/Router1/tls/domains/1/main` | `foobar` | +| `traefik/http/routers/Router1/tls/domains/1/sans/0` | `foobar` | +| `traefik/http/routers/Router1/tls/domains/1/sans/1` | `foobar` | +| `traefik/http/routers/Router1/tls/options` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name1` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/hostname` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/interval` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/path` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/port` | `42` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/scheme` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/timeout` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/passHostHeader` | `true` | +| `traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/servers/0/url` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/servers/1/url` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/sticky/cookie/httpOnly` | `true` | +| `traefik/http/services/Service01/loadBalancer/sticky/cookie/name` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/sticky/cookie/secure` | `true` | +| `traefik/http/services/Service02/mirroring/mirrors/0/name` | `foobar` | +| `traefik/http/services/Service02/mirroring/mirrors/0/percent` | `42` | +| `traefik/http/services/Service02/mirroring/mirrors/1/name` | `foobar` | +| `traefik/http/services/Service02/mirroring/mirrors/1/percent` | `42` | +| `traefik/http/services/Service02/mirroring/service` | `foobar` | +| `traefik/http/services/Service03/weighted/services/0/name` | `foobar` | +| `traefik/http/services/Service03/weighted/services/0/weight` | `42` | +| `traefik/http/services/Service03/weighted/services/1/name` | `foobar` | +| `traefik/http/services/Service03/weighted/services/1/weight` | `42` | +| `traefik/http/services/Service03/weighted/sticky/cookie/httpOnly` | `true` | +| `traefik/http/services/Service03/weighted/sticky/cookie/name` | `foobar` | +| `traefik/http/services/Service03/weighted/sticky/cookie/secure` | `true` | +| `traefik/tcp/routers/TCPRouter0/entryPoints/0` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/entryPoints/1` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/rule` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/service` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/certResolver` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/domains/0/main` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/domains/0/sans/0` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/domains/0/sans/1` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/domains/1/main` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/domains/1/sans/0` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/domains/1/sans/1` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/options` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/tls/passthrough` | `true` | +| `traefik/tcp/routers/TCPRouter1/entryPoints/0` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/entryPoints/1` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/rule` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/service` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/certResolver` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/domains/0/main` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/domains/0/sans/0` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/domains/0/sans/1` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/domains/1/main` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/domains/1/sans/0` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/domains/1/sans/1` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/options` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/tls/passthrough` | `true` | +| `traefik/tcp/services/TCPService01/loadBalancer/servers/0/address` | `foobar` | +| `traefik/tcp/services/TCPService01/loadBalancer/servers/1/address` | `foobar` | +| `traefik/tcp/services/TCPService01/loadBalancer/terminationDelay` | `42` | +| `traefik/tcp/services/TCPService02/weighted/services/0/name` | `foobar` | +| `traefik/tcp/services/TCPService02/weighted/services/0/weight` | `42` | +| `traefik/tcp/services/TCPService02/weighted/services/1/name` | `foobar` | +| `traefik/tcp/services/TCPService02/weighted/services/1/weight` | `42` | +| `traefik/tls/certificates/0/certFile` | `foobar` | +| `traefik/tls/certificates/0/keyFile` | `foobar` | +| `traefik/tls/certificates/0/stores/0` | `foobar` | +| `traefik/tls/certificates/0/stores/1` | `foobar` | +| `traefik/tls/certificates/1/certFile` | `foobar` | +| `traefik/tls/certificates/1/keyFile` | `foobar` | +| `traefik/tls/certificates/1/stores/0` | `foobar` | +| `traefik/tls/certificates/1/stores/1` | `foobar` | +| `traefik/tls/options/Options0/cipherSuites/0` | `foobar` | +| `traefik/tls/options/Options0/cipherSuites/1` | `foobar` | +| `traefik/tls/options/Options0/clientAuth/caFiles/0` | `foobar` | +| `traefik/tls/options/Options0/clientAuth/caFiles/1` | `foobar` | +| `traefik/tls/options/Options0/clientAuth/clientAuthType` | `foobar` | +| `traefik/tls/options/Options0/curvePreferences/0` | `foobar` | +| `traefik/tls/options/Options0/curvePreferences/1` | `foobar` | +| `traefik/tls/options/Options0/maxVersion` | `foobar` | +| `traefik/tls/options/Options0/minVersion` | `foobar` | +| `traefik/tls/options/Options0/sniStrict` | `true` | +| `traefik/tls/options/Options1/cipherSuites/0` | `foobar` | +| `traefik/tls/options/Options1/cipherSuites/1` | `foobar` | +| `traefik/tls/options/Options1/clientAuth/caFiles/0` | `foobar` | +| `traefik/tls/options/Options1/clientAuth/caFiles/1` | `foobar` | +| `traefik/tls/options/Options1/clientAuth/clientAuthType` | `foobar` | +| `traefik/tls/options/Options1/curvePreferences/0` | `foobar` | +| `traefik/tls/options/Options1/curvePreferences/1` | `foobar` | +| `traefik/tls/options/Options1/maxVersion` | `foobar` | +| `traefik/tls/options/Options1/minVersion` | `foobar` | +| `traefik/tls/options/Options1/sniStrict` | `true` | +| `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` | +| `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `foobar` | +| `traefik/tls/stores/Store1/defaultCertificate/certFile` | `foobar` | +| `traefik/tls/stores/Store1/defaultCertificate/keyFile` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/kv.md b/docs/content/reference/dynamic-configuration/kv.md new file mode 100644 index 000000000..7c4d9c656 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/kv.md @@ -0,0 +1,8 @@ +# KV Configuration Reference + +Dynamic configuration with KV stores. +{: .subtitle } + +| Key (Path) | Value | +|----------------------------------------------------------------------------------------------|-------------| +--8<-- "content/reference/dynamic-configuration/kv-ref.md" diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 2d80ee2e2..f47ac4655 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -243,6 +243,36 @@ EntryPoint (Default: ```traefik```) `--ping.manualrouting`: Manual routing (Default: ```false```) +`--providers.consul`: +Enable Consul backend with default settings. (Default: ```false```) + +`--providers.consul.endpoints`: +KV store endpoints (Default: ```127.0.0.1:8500```) + +`--providers.consul.password`: +KV Password + +`--providers.consul.rootkey`: +Root key used for KV store (Default: ```traefik```) + +`--providers.consul.tls.ca`: +TLS CA + +`--providers.consul.tls.caoptional`: +TLS CA.Optional (Default: ```false```) + +`--providers.consul.tls.cert`: +TLS cert + +`--providers.consul.tls.insecureskipverify`: +TLS insecure skip verify (Default: ```false```) + +`--providers.consul.tls.key`: +TLS key + +`--providers.consul.username`: +KV Username + `--providers.consulcatalog.cache`: Use local agent caching for catalog reads. (Default: ```false```) @@ -348,6 +378,36 @@ Use the ip address from the bound port, rather than from the inner network. (Def `--providers.docker.watch`: Watch provider. (Default: ```true```) +`--providers.etcd`: +Enable Etcd backend with default settings. (Default: ```false```) + +`--providers.etcd.endpoints`: +KV store endpoints (Default: ```127.0.0.1:2379```) + +`--providers.etcd.password`: +KV Password + +`--providers.etcd.rootkey`: +Root key used for KV store (Default: ```traefik```) + +`--providers.etcd.tls.ca`: +TLS CA + +`--providers.etcd.tls.caoptional`: +TLS CA.Optional (Default: ```false```) + +`--providers.etcd.tls.cert`: +TLS cert + +`--providers.etcd.tls.insecureskipverify`: +TLS insecure skip verify (Default: ```false```) + +`--providers.etcd.tls.key`: +TLS key + +`--providers.etcd.username`: +KV Username + `--providers.file.debugloggeneratedtemplate`: Enable debug logging of generated configuration template. (Default: ```false```) @@ -516,12 +576,72 @@ Defines the polling interval in seconds. (Default: ```15```) `--providers.rancher.watch`: Watch provider. (Default: ```true```) +`--providers.redis`: +Enable Redis backend with default settings. (Default: ```false```) + +`--providers.redis.endpoints`: +KV store endpoints (Default: ```127.0.0.1:6379```) + +`--providers.redis.password`: +KV Password + +`--providers.redis.rootkey`: +Root key used for KV store (Default: ```traefik```) + +`--providers.redis.tls.ca`: +TLS CA + +`--providers.redis.tls.caoptional`: +TLS CA.Optional (Default: ```false```) + +`--providers.redis.tls.cert`: +TLS cert + +`--providers.redis.tls.insecureskipverify`: +TLS insecure skip verify (Default: ```false```) + +`--providers.redis.tls.key`: +TLS key + +`--providers.redis.username`: +KV Username + `--providers.rest`: Enable Rest backend with default settings. (Default: ```false```) `--providers.rest.insecure`: Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```) +`--providers.zookeeper`: +Enable ZooKeeper backend with default settings. (Default: ```false```) + +`--providers.zookeeper.endpoints`: +KV store endpoints (Default: ```127.0.0.1:2181```) + +`--providers.zookeeper.password`: +KV Password + +`--providers.zookeeper.rootkey`: +Root key used for KV store (Default: ```traefik```) + +`--providers.zookeeper.tls.ca`: +TLS CA + +`--providers.zookeeper.tls.caoptional`: +TLS CA.Optional (Default: ```false```) + +`--providers.zookeeper.tls.cert`: +TLS cert + +`--providers.zookeeper.tls.insecureskipverify`: +TLS insecure skip verify (Default: ```false```) + +`--providers.zookeeper.tls.key`: +TLS key + +`--providers.zookeeper.username`: +KV Username + `--serverstransport.forwardingtimeouts.dialtimeout`: The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists. (Default: ```30```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 8689d7bc4..ddceb31e8 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -243,6 +243,9 @@ EntryPoint (Default: ```traefik```) `TRAEFIK_PING_MANUALROUTING`: Manual routing (Default: ```false```) +`TRAEFIK_PROVIDERS_CONSUL`: +Enable Consul backend with default settings. (Default: ```false```) + `TRAEFIK_PROVIDERS_CONSULCATALOG_CACHE`: Use local agent caching for catalog reads. (Default: ```false```) @@ -303,6 +306,33 @@ Forces the read to be fully consistent. (Default: ```false```) `TRAEFIK_PROVIDERS_CONSULCATALOG_STALE`: Use stale consistency for catalog reads. (Default: ```false```) +`TRAEFIK_PROVIDERS_CONSUL_ENDPOINTS`: +KV store endpoints (Default: ```127.0.0.1:8500```) + +`TRAEFIK_PROVIDERS_CONSUL_PASSWORD`: +KV Password + +`TRAEFIK_PROVIDERS_CONSUL_ROOTKEY`: +Root key used for KV store (Default: ```traefik```) + +`TRAEFIK_PROVIDERS_CONSUL_TLS_CA`: +TLS CA + +`TRAEFIK_PROVIDERS_CONSUL_TLS_CAOPTIONAL`: +TLS CA.Optional (Default: ```false```) + +`TRAEFIK_PROVIDERS_CONSUL_TLS_CERT`: +TLS cert + +`TRAEFIK_PROVIDERS_CONSUL_TLS_INSECURESKIPVERIFY`: +TLS insecure skip verify (Default: ```false```) + +`TRAEFIK_PROVIDERS_CONSUL_TLS_KEY`: +TLS key + +`TRAEFIK_PROVIDERS_CONSUL_USERNAME`: +KV Username + `TRAEFIK_PROVIDERS_DOCKER`: Enable Docker backend with default settings. (Default: ```false```) @@ -348,6 +378,36 @@ Use the ip address from the bound port, rather than from the inner network. (Def `TRAEFIK_PROVIDERS_DOCKER_WATCH`: Watch provider. (Default: ```true```) +`TRAEFIK_PROVIDERS_ETCD`: +Enable Etcd backend with default settings. (Default: ```false```) + +`TRAEFIK_PROVIDERS_ETCD_ENDPOINTS`: +KV store endpoints (Default: ```127.0.0.1:2379```) + +`TRAEFIK_PROVIDERS_ETCD_PASSWORD`: +KV Password + +`TRAEFIK_PROVIDERS_ETCD_ROOTKEY`: +Root key used for KV store (Default: ```traefik```) + +`TRAEFIK_PROVIDERS_ETCD_TLS_CA`: +TLS CA + +`TRAEFIK_PROVIDERS_ETCD_TLS_CAOPTIONAL`: +TLS CA.Optional (Default: ```false```) + +`TRAEFIK_PROVIDERS_ETCD_TLS_CERT`: +TLS cert + +`TRAEFIK_PROVIDERS_ETCD_TLS_INSECURESKIPVERIFY`: +TLS insecure skip verify (Default: ```false```) + +`TRAEFIK_PROVIDERS_ETCD_TLS_KEY`: +TLS key + +`TRAEFIK_PROVIDERS_ETCD_USERNAME`: +KV Username + `TRAEFIK_PROVIDERS_FILE_DEBUGLOGGENERATEDTEMPLATE`: Enable debug logging of generated configuration template. (Default: ```false```) @@ -516,12 +576,72 @@ Defines the polling interval in seconds. (Default: ```15```) `TRAEFIK_PROVIDERS_RANCHER_WATCH`: Watch provider. (Default: ```true```) +`TRAEFIK_PROVIDERS_REDIS`: +Enable Redis backend with default settings. (Default: ```false```) + +`TRAEFIK_PROVIDERS_REDIS_ENDPOINTS`: +KV store endpoints (Default: ```127.0.0.1:6379```) + +`TRAEFIK_PROVIDERS_REDIS_PASSWORD`: +KV Password + +`TRAEFIK_PROVIDERS_REDIS_ROOTKEY`: +Root key used for KV store (Default: ```traefik```) + +`TRAEFIK_PROVIDERS_REDIS_TLS_CA`: +TLS CA + +`TRAEFIK_PROVIDERS_REDIS_TLS_CAOPTIONAL`: +TLS CA.Optional (Default: ```false```) + +`TRAEFIK_PROVIDERS_REDIS_TLS_CERT`: +TLS cert + +`TRAEFIK_PROVIDERS_REDIS_TLS_INSECURESKIPVERIFY`: +TLS insecure skip verify (Default: ```false```) + +`TRAEFIK_PROVIDERS_REDIS_TLS_KEY`: +TLS key + +`TRAEFIK_PROVIDERS_REDIS_USERNAME`: +KV Username + `TRAEFIK_PROVIDERS_REST`: Enable Rest backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_REST_INSECURE`: Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```) +`TRAEFIK_PROVIDERS_ZOOKEEPER`: +Enable ZooKeeper backend with default settings. (Default: ```false```) + +`TRAEFIK_PROVIDERS_ZOOKEEPER_ENDPOINTS`: +KV store endpoints (Default: ```127.0.0.1:2181```) + +`TRAEFIK_PROVIDERS_ZOOKEEPER_PASSWORD`: +KV Password + +`TRAEFIK_PROVIDERS_ZOOKEEPER_ROOTKEY`: +Root key used for KV store (Default: ```traefik```) + +`TRAEFIK_PROVIDERS_ZOOKEEPER_TLS_CA`: +TLS CA + +`TRAEFIK_PROVIDERS_ZOOKEEPER_TLS_CAOPTIONAL`: +TLS CA.Optional (Default: ```false```) + +`TRAEFIK_PROVIDERS_ZOOKEEPER_TLS_CERT`: +TLS cert + +`TRAEFIK_PROVIDERS_ZOOKEEPER_TLS_INSECURESKIPVERIFY`: +TLS insecure skip verify (Default: ```false```) + +`TRAEFIK_PROVIDERS_ZOOKEEPER_TLS_KEY`: +TLS key + +`TRAEFIK_PROVIDERS_ZOOKEEPER_USERNAME`: +KV Username + `TRAEFIK_SERVERSTRANSPORT_FORWARDINGTIMEOUTS_DIALTIMEOUT`: The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists. (Default: ```30```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index edb9b757b..b2d78570b 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -129,6 +129,50 @@ cert = "foobar" key = "foobar" insecureSkipVerify = true + [providers.consul] + rootKey = "traefik" + endpoints = ["foobar", "foobar"] + username = "foobar" + password = "foobar" + [providers.consul.tls] + ca = "foobar" + caOptional = true + cert = "foobar" + key = "foobar" + insecureSkipVerify = true + [providers.etcd] + rootKey = "traefik" + endpoints = ["foobar", "foobar"] + username = "foobar" + password = "foobar" + [providers.etcd.tls] + ca = "foobar" + caOptional = true + cert = "foobar" + key = "foobar" + insecureSkipVerify = true + [providers.zooKeeper] + rootKey = "traefik" + endpoints = ["foobar", "foobar"] + username = "foobar" + password = "foobar" + [providers.zooKeeper.tls] + ca = "foobar" + caOptional = true + cert = "foobar" + key = "foobar" + insecureSkipVerify = true + [providers.redis] + rootKey = "traefik" + endpoints = ["foobar", "foobar"] + username = "foobar" + password = "foobar" + [providers.redis.tls] + ca = "foobar" + caOptional = true + cert = "foobar" + key = "foobar" + insecureSkipVerify = true [api] insecure = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 137c23233..3c3bf2470 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -136,6 +136,58 @@ providers: cert: foobar key: foobar insecureSkipVerify: true + consul: + rootKey: traefik + endpoints: + - foobar + - foobar + username: foobar + password: foobar + tls: + ca: foobar + caOptional: true + cert: foobar + key: foobar + insecureSkipVerify: true + etcd: + rootKey: traefik + endpoints: + - foobar + - foobar + username: foobar + password: foobar + tls: + ca: foobar + caOptional: true + cert: foobar + key: foobar + insecureSkipVerify: true + zooKeeper: + rootKey: traefik + endpoints: + - foobar + - foobar + username: foobar + password: foobar + tls: + ca: foobar + caOptional: true + cert: foobar + key: foobar + insecureSkipVerify: true + redis: + rootKey: traefik + endpoints: + - foobar + - foobar + username: foobar + password: foobar + tls: + ca: foobar + caOptional: true + cert: foobar + key: foobar + insecureSkipVerify: true api: insecure: true dashboard: true diff --git a/docs/content/routing/providers/kv.md b/docs/content/routing/providers/kv.md new file mode 100644 index 000000000..4ff13aa1e --- /dev/null +++ b/docs/content/routing/providers/kv.md @@ -0,0 +1,392 @@ +# Traefik & KV Stores + +A Story of key & values +{: .subtitle } + +## Routing Configuration + +!!! info "Keys" + + - Keys are case insensitive. + - The complete list of keys can be found in [the reference page](../../reference/dynamic-configuration/kv.md). + +### Routers + +!!! warning "The character `@` is not authorized in the router name ``." + +??? info "`traefik/http/routers//rule`" + + See [rule](../routers/index.md#rule) for more information. + + | Key (Path) | Value | + |--------------------------------------|----------------------------| + | `traefik/http/routers/myrouter/rule` | ```Host(`mydomain.com`)``` | + +??? info "`traefik/http/routers//entrypoints`" + + See [entry points](../routers/index.md#entrypoints) for more information. + + | Key (Path) | Value | + |-----------------------------------------------|-------------| + | `traefik.http.routers.myrouter.entrypoints/0` | `web` | + | `traefik.http.routers.myrouter.entrypoints/1` | `websecure` | + +??? info "`traefik/http/routers//middlewares`" + + See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information. + + | Key (Path) | Value | + |-----------------------------------------------|-------------| + | `traefik/http/routers/myrouter/middlewares/0` | `auth` | + | `traefik/http/routers/myrouter/middlewares/1` | `prefix` | + | `traefik/http/routers/myrouter/middlewares/2` | `cb` | + +??? info "`traefik/http/routers//service`" + + See [rule](../routers/index.md#service) for more information. + + | Key (Path) | Value | + |-----------------------------------------|-------------| + | `traefik/http/routers/myrouter/service` | `myservice` | + +??? info "`traefik/http/routers//tls`" + + See [tls](../routers/index.md#tls) for more information. + + | Key (Path) | Value | + |-------------------------------------|--------| + | `traefik/http/routers/myrouter/tls` | `true` | + +??? info "`traefik/http/routers//tls/certresolver`" + + See [certResolver](../routers/index.md#certresolver) for more information. + + | Key (Path) | Value | + |--------------------------------------------------|--------------| + | `traefik/http/routers/myrouter/tls/certresolver` | `myresolver` | + +??? info "`traefik/http/routers//tls/domains//main`" + + See [domains](../routers/index.md#domains) for more information. + + | Key (Path) | Value | + |----------------------------------------------------|--------------| + | `traefik/http/routers/myrouter/tls/domains/0/main` | `foobar.com` | + +??? info "`traefik/http/routers//tls/domains//sans/`" + + See [domains](../routers/index.md#domains) for more information. + + | Key (Path) | Value | + |------------------------------------------------------|-------------------| + | `traefik/http/routers/myrouter/tls/domains/0/sans/0` | `test.foobar.com` | + | `traefik/http/routers/myrouter/tls/domains/0/sans/1` | `dev.foobar.com` | + +??? info "`traefik/http/routers//tls/options`" + + See [options](../routers/index.md#options) for more information. + + | Key (Path) | Value | + |---------------------------------------------|----------| + | `traefik/http/routers/myrouter/tls/options` | `foobar` | + +??? info "`traefik/http/routers//priority`" + + See [priority](../routers/index.md#priority) for more information. + + | Key (Path) | Value | + |------------------------------------------|-------| + | `traefik/http/routers/myrouter/priority` | `42` | + +### Services + +!!! warning "The character `@` is not authorized in the service name ``." + +??? info "`traefik/http/services//loadbalancer/servers//url`" + + See [servers](../services/index.md#servers) for more information. + + | Key (Path) | Value | + |-----------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/servers/0/scheme` | `http` | + +??? info "`traefik/http/services//loadbalancer/servers//scheme`" + + Overrides the default scheme. + + | Key (Path) | Value | + |-----------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/servers/0/scheme` | `http` | + +??? info "`traefik/http/services//loadbalancer/passhostheader`" + + See [pass Host header](../services/index.md#pass-host-header) for more information. + + | Key (Path) | Value | + |-----------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/passhostheader` | `true` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/headers/`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |--------------------------------------------------------------------------|----------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/headers/X-Foo` | `foobar` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/hostname`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |---------------------------------------------------------------------|--------------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/hostname` | `foobar.com` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/interval`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |---------------------------------------------------------------------|-------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/interval` | `10` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/path`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |-----------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/path` | `/foo` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/port`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |-----------------------------------------------------------------|-------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/port` | `42` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/scheme`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |-------------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/scheme` | `http` | + +??? info "`traefik/http/services//loadbalancer/healthcheck/timeout`" + + See [health check](../services/index.md#health-check) for more information. + + | Key (Path) | Value | + |--------------------------------------------------------------------|-------| + | `traefik/http/services/myservice/loadbalancer/healthcheck/timeout` | `10` | + +??? info "`traefik/http/services//loadbalancer/sticky`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + | Key (Path) | Value | + |-------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/sticky` | `true` | + +??? info "`traefik/http/services//loadbalancer/sticky/cookie/httponly`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + | Key (Path) | Value | + |-----------------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/sticky/cookie/httponly` | `true` | + +??? info "`traefik/http/services//loadbalancer/sticky/cookie/name`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + | Key (Path) | Value | + |-------------------------------------------------------------------|----------| + | `traefik/http/services/myservice/loadbalancer/sticky/cookie/name` | `foobar` | + +??? info "`traefik/http/services//loadbalancer/sticky/cookie/secure`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + | Key (Path) | Value | + |---------------------------------------------------------------------|--------| + | `traefik/http/services/myservice/loadbalancer/sticky/cookie/secure` | `true` | + +??? info "`traefik/http/services//loadbalancer/responseforwarding/flushinterval`" + + See [response forwarding](../services/index.md#response-forwarding) for more information. + + | Key (Path) | Value | + |---------------------------------------------------------------------------------|-------| + | `traefik/http/services/myservice/loadbalancer/responseforwarding/flushinterval` | `10` | + +??? info "`traefik/http/services//mirroring/service`" + + | Key (Path) | Value | + |----------------------------------------------------------|----------| + | `traefik/http/services//mirroring/service` | `foobar` | + +??? info "`traefik/http/services//mirroring/mirrors//name`" + + | Key (Path) | Value | + |-------------------------------------------------------------------|----------| + | `traefik/http/services//mirroring/mirrors//name` | `foobar` | + +??? info "`traefik/http/services//mirroring/mirrors//percent`" + + | Key (Path) | Value | + |----------------------------------------------------------------------|-------| + | `traefik/http/services//mirroring/mirrors//percent` | `42` | + +??? info "`traefik/http/services//weighted/services//name`" + + | Key (Path) | Value | + |-------------------------------------------------------------------|----------| + | `traefik/http/services//weighted/services//name` | `foobar` | + +??? info "`traefik/http/services//weighted/services//weight`" + + | Key (Path) | Value | + |---------------------------------------------------------------------|-------| + | `traefik/http/services//weighted/services//weight` | `42` | + +??? info "`traefik/http/services//weighted/sticky/cookie/name`" + + | Key (Path) | Value | + |--------------------------------------------------------------------|----------| + | `traefik/http/services//weighted/sticky/cookie/name` | `foobar` | + +??? info "`traefik/http/services//weighted/sticky/cookie/secure`" + + | Key (Path) | Value | + |----------------------------------------------------------------------|--------| + | `traefik/http/services//weighted/sticky/cookie/secure` | `true` | + +??? info "`traefik/http/services//weighted/sticky/cookie/httpOnly`" + + | Key (Path) | Value | + |------------------------------------------------------------------------|--------| + | `traefik/http/services//weighted/sticky/cookie/httpOnly` | `true` | + +### Middleware + +More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md). + +!!! warning "The character `@` is not authorized in the middleware name." + +!!! warning "Conflicts in Declaration" + + If you declare multiple middleware with the same name but with different parameters, the middleware fails to be declared. + +### TCP + +You can declare TCP Routers and/or Services using KV. + +#### TCP Routers + +??? info "`traefik/tcp/routers//entrypoints`" + + See [entry points](../routers/index.md#entrypoints_1) for more information. + + | Key (Path) | Value | + |-------------------------------------------------|-------| + | `traefik/tcp/routers/mytcprouter/entrypoints/0` | `ep1` | + | `traefik/tcp/routers/mytcprouter/entrypoints/1` | `ep2` | + +??? info "`traefik/tcp/routers//rule`" + + See [rule](../routers/index.md#rule_1) for more information. + + | Key (Path) | Value | + |--------------------------------------|------------------------------| + | `traefik/tcp/routers/my-router/rule` | ```HostSNI(`my-host.com`)``` | + +??? info "`traefik/tcp/routers//service`" + + See [service](../routers/index.md#services) for more information. + + | Key (Path) | Value | + |-------------------------------------------|-------------| + | `traefik/tcp/routers/mytcprouter/service` | `myservice` | + +??? info "`traefik/tcp/routers//tls`" + + See [TLS](../routers/index.md#tls_1) for more information. + + | Key (Path) | Value | + |---------------------------------------|--------| + | `traefik/tcp/routers/mytcprouter/tls` | `true` | + +??? info "`traefik/tcp/routers//tls/certresolver`" + + See [certResolver](../routers/index.md#certresolver_1) for more information. + + | Key (Path) | Value | + |----------------------------------------------------|--------------| + | `traefik/tcp/routers/mytcprouter/tls/certresolver` | `myresolver` | + +??? info "`traefik/tcp/routers//tls/domains//main`" + + See [domains](../routers/index.md#domains_1) for more information. + + | Key (Path) | Value | + |------------------------------------------------------|--------------| + | `traefik/tcp/routers/mytcprouter/tls/domains/0/main` | `foobar.com` | + +??? info "`traefik/tcp/routers//tls/domains//sans`" + + See [domains](../routers/index.md#domains_1) for more information. + + | Key (Path) | Value | + |--------------------------------------------------------|-------------------| + | `traefik/tcp/routers/mytcprouter/tls/domains/0/sans/0` | `test.foobar.com` | + | `traefik/tcp/routers/mytcprouter/tls/domains/0/sans/1` | `dev.foobar.com` | + +??? info "`traefik/tcp/routers//tls/options`" + + See [options](../routers/index.md#options_1) for more information. + + | Key (Path) | Value | + |-----------------------------------------------|----------| + | `traefik/tcp/routers/mytcprouter/tls/options` | `foobar` | + + +??? info "`traefik/tcp/routers//tls/passthrough`" + + See [TLS](../routers/index.md#tls_1) for more information. + + | Key (Path) | Value | + |---------------------------------------------------|--------| + | `traefik/tcp/routers/mytcprouter/tls/passthrough` | `true` | + +#### TCP Services + +??? info "`traefik/tcp/services//loadbalancer/servers//url`" + + See [servers](../services/index.md#servers) for more information. + + | Key (Path) | Value | + |-------------------------------------------------------------------|--------| + | `traefik/tcp/services/mytcpservice/loadbalancer/servers/0/scheme` | `http` | + +??? info "`traefik/tcp/services//loadbalancer/terminationdelay`" + + See [termination delay](../services/index.md#termination-delay) for more information. + + | Key (Path) | Value | + |-------------------------------------------------------------------|-------| + | `traefik/tcp/services/mytcpservice/loadbalancer/terminationdelay` | `100` | + +??? info "`traefik/tcp/services//weighted/services//name`" + + | Key (Path) | Value | + |---------------------------------------------------------------------|----------| + | `traefik/tcp/services//weighted/services/0/name` | `foobar` | + +??? info "`traefik/tcp/services//weighted/services//weight`" + + | Key (Path) | Value | + |------------------------------------------------------------------|-------| + | `traefik/tcp/services//weighted/services/0/weight` | `42` | diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9de272501..601c33e7d 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -84,6 +84,10 @@ nav: - 'Marathon': 'providers/marathon.md' - 'Rancher': 'providers/rancher.md' - 'File': 'providers/file.md' + - 'Consul': 'providers/consul.md' + - 'Etcd': 'providers/etcd.md' + - 'ZooKeeper': 'providers/zookeeper.md' + - 'Redis': 'providers/redis.md' - 'Routing & Load Balancing': - 'Overview': 'routing/overview.md' - 'EntryPoints': 'routing/entrypoints.md' @@ -95,6 +99,7 @@ nav: - 'Consul Catalog': 'routing/providers/consul-catalog.md' - 'Marathon': 'routing/providers/marathon.md' - 'Rancher': 'routing/providers/rancher.md' + - 'KV': 'routing/providers/kv.md' - 'HTTPS & TLS': - 'Overview': 'https/overview.md' - 'TLS': 'https/tls.md' @@ -180,3 +185,4 @@ nav: - 'Consul Catalog': 'reference/dynamic-configuration/consul-catalog.md' - 'Marathon': 'reference/dynamic-configuration/marathon.md' - 'Rancher': 'reference/dynamic-configuration/rancher.md' + - 'KV': 'reference/dynamic-configuration/kv.md' diff --git a/go.sum b/go.sum index 96842312b..5f8707798 100644 --- a/go.sum +++ b/go.sum @@ -120,11 +120,15 @@ github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba h1:PhR03pep+5e github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba/go.mod h1:zkWcASFUJEst6QwCrxLdkuw1gvaKqmflEipm+iecV5M= github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 h1:1srn9voikJGofblBhWy3WuZWqo14Ou7NaswNG/I2yWc= github.com/containous/mux v0.0.0-20181024131434-c33f32e26898/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg= +github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpu/goacmedns v0.0.1 h1:GeIU5chKys9zmHgOAgP+bstRaLqcGQ6HJh/hLw9hrus= github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok= @@ -198,6 +202,7 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2 h1:df6OFl8WNXk82xxP3R9ZPZ5seOA8XZkwLdbEzZF1/xI= github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2/go.mod h1:GLyXJD41gBO/NPKVPGQbhyyC06eugGy15QEZyUkE2/s= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v3 v3.2.0 h1:z0zvNlL1niv/1qA06V5X1BRC5PeLoGKAlVaWthXQz9c= github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE= @@ -279,7 +284,9 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f h1:68WxnfBzJRYktZ30fmIjGQ74RsXYLoeH2/NITPktTMY= github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -527,6 +534,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sacloud/libsacloud v1.26.1 h1:td3Kd7lvpSAxxHEVpnaZ9goHmmhi0D/RfP0Rqqf/kek= github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ= +github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec h1:6ncX5ko6B9LntYM0YBRXkiSaZMmLYeZ/NWcmeB43mMY= github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= @@ -543,6 +551,7 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -561,6 +570,7 @@ github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7 h1:CpHxIaZzVy26G github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= github.com/tinylib/msgp v1.0.2 h1:DfdQrzQa7Yh2es9SuLkixqxuXS2SxsdYn0KbdrOGWD8= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY= github.com/transip/gotransip v5.8.2+incompatible h1:aNJhw/w/3QBqFcHAIPz1ytoK5FexeMzbUCGrrhWr3H0= @@ -593,6 +603,7 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.elastic.co/apm v1.6.0 h1:RzyNj9Qx2iXh4A8DIg/aMUdtwGFNw3R8sKO3/Hf4pqk= go.elastic.co/apm v1.6.0/go.mod h1:/VByR6FBtuNu1YnPHz7Gri7YiIqAoFBGVp+2xkSE8tI= @@ -602,7 +613,9 @@ go.elastic.co/apm/module/apmot v1.6.0 h1:3Tjwu25SfB2Bq+UsO8JOKYOuMzUu4s8/fS8BGkl go.elastic.co/apm/module/apmot v1.6.0/go.mod h1:JIGFfrixcGjp8oD22PTTCvdNMfp+O+lOb5Hw4MtqxQ4= go.elastic.co/fastjson v1.0.0 h1:ooXV/ABvf+tBul26jcVViPT3sBir0PvXgibYB1IQQzg= go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHtchs= +go.etcd.io/bbolt v1.3.1-etcd.8 h1:6J7QAKqfFBGnU80KRnuQxfjjeE5xAGE/qB810I3FQHQ= go.etcd.io/bbolt v1.3.1-etcd.8/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v3.3.13+incompatible h1:jCejD5EMnlGxFvcGRyEV4VGlENZc7oPQX6o0t7n3xbw= go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -612,9 +625,11 @@ go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 h1:d9qaMM+ODpCq+9We41//fu/sHsTnXcrqd1en3x+GKy4= go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -769,6 +784,7 @@ gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc h1:GAcf+t0o8gdJAdSFYdE9wChu4bIyguMVqz0RHiFL5VY= gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= +gopkg.in/redis.v5 v5.2.9 h1:MNZYOLPomQzZMfpN3ZtD1uyJ2IDonTTlxYiV/pEApiw= gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY= gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= diff --git a/integration/consul_test.go b/integration/consul_test.go new file mode 100644 index 000000000..d456acbc8 --- /dev/null +++ b/integration/consul_test.go @@ -0,0 +1,159 @@ +package integration + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/abronan/valkeyrie" + "github.com/abronan/valkeyrie/store" + "github.com/abronan/valkeyrie/store/consul" + "github.com/containous/traefik/v2/integration/try" + "github.com/containous/traefik/v2/pkg/api" + "github.com/go-check/check" + "github.com/pmezard/go-difflib/difflib" + checker "github.com/vdemeester/shakers" +) + +// Consul test suites (using libcompose) +type ConsulSuite struct { + BaseSuite + kvClient store.Store +} + +func (s *ConsulSuite) setupStore(c *check.C) { + s.createComposeProject(c, "consul") + s.composeProject.Start(c) + + consul.Register() + kv, err := valkeyrie.NewStore( + store.CONSUL, + []string{s.composeProject.Container(c, "consul").NetworkSettings.IPAddress + ":8500"}, + &store.Config{ + ConnectionTimeout: 10 * time.Second, + }, + ) + if err != nil { + c.Fatal("Cannot create store consul") + } + s.kvClient = kv + + // wait for consul + err = try.Do(60*time.Second, try.KVExists(kv, "test")) + c.Assert(err, checker.IsNil) +} + +func (s *ConsulSuite) TearDownTest(c *check.C) { + // shutdown and delete compose project + if s.composeProject != nil { + s.composeProject.Stop(c) + } +} + +func (s *ConsulSuite) TearDownSuite(c *check.C) {} + +func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) { + s.setupStore(c) + + address := "http://" + s.composeProject.Container(c, "consul").NetworkSettings.IPAddress + ":8500" + file := s.adaptFile(c, "fixtures/consul/simple.toml", struct{ ConsulAddress string }{address}) + defer os.Remove(file) + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + data := map[string]string{ + "traefik/http/routers/Router0/entryPoints/0": "web", + "traefik/http/routers/Router0/middlewares/0": "compressor", + "traefik/http/routers/Router0/middlewares/1": "striper", + "traefik/http/routers/Router0/service": "simplesvc", + "traefik/http/routers/Router0/rule": "Host(`kv1.localhost`)", + "traefik/http/routers/Router0/priority": "42", + "traefik/http/routers/Router0/tls": "", + + "traefik/http/routers/Router1/rule": "Host(`kv2.localhost`)", + "traefik/http/routers/Router1/priority": "42", + "traefik/http/routers/Router1/tls/domains/0/main": "aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/0": "aaa.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/1": "bbb.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/1/main": "bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/0": "aaa.bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/1": "bbb.bbb.localhost", + "traefik/http/routers/Router1/entryPoints/0": "web", + "traefik/http/routers/Router1/service": "simplesvc", + + "traefik/http/services/simplesvc/loadBalancer/servers/0/url": "http://10.0.1.1:8888", + "traefik/http/services/simplesvc/loadBalancer/servers/1/url": "http://10.0.1.1:8889", + + "traefik/http/services/srvcA/loadBalancer/servers/0/url": "http://10.0.1.2:8888", + "traefik/http/services/srvcA/loadBalancer/servers/1/url": "http://10.0.1.2:8889", + + "traefik/http/services/srvcB/loadBalancer/servers/0/url": "http://10.0.1.3:8888", + "traefik/http/services/srvcB/loadBalancer/servers/1/url": "http://10.0.1.3:8889", + + "traefik/http/services/mirror/mirroring/service": "simplesvc", + "traefik/http/services/mirror/mirroring/mirrors/0/name": "srvcA", + "traefik/http/services/mirror/mirroring/mirrors/0/percent": "42", + "traefik/http/services/mirror/mirroring/mirrors/1/name": "srvcB", + "traefik/http/services/mirror/mirroring/mirrors/1/percent": "42", + + "traefik/http/services/Service03/weighted/services/0/name": "srvcA", + "traefik/http/services/Service03/weighted/services/0/weight": "42", + "traefik/http/services/Service03/weighted/services/1/name": "srvcB", + "traefik/http/services/Service03/weighted/services/1/weight": "42", + + "traefik/http/middlewares/compressor/compress": "", + "traefik/http/middlewares/striper/stripPrefix/prefixes/0": "foo", + "traefik/http/middlewares/striper/stripPrefix/prefixes/1": "bar", + "traefik/http/middlewares/striper/stripPrefix/forceSlash": "true", + } + + for k, v := range data { + err := s.kvClient.Put(k, []byte(v), nil) + c.Assert(err, checker.IsNil) + } + + // wait for traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("@consul")) + c.Assert(err, checker.IsNil) + + resp, err := http.Get("http://127.0.0.1:8080/api/rawdata") + c.Assert(err, checker.IsNil) + + var obtained api.RunTimeRepresentation + err = json.NewDecoder(resp.Body).Decode(&obtained) + c.Assert(err, checker.IsNil) + got, err := json.MarshalIndent(obtained, "", " ") + c.Assert(err, checker.IsNil) + + expectedJSON := filepath.FromSlash("testdata/rawdata-consul.json") + + if *updateExpected { + err = ioutil.WriteFile(expectedJSON, got, 0666) + c.Assert(err, checker.IsNil) + } + + expected, err := ioutil.ReadFile(expectedJSON) + c.Assert(err, checker.IsNil) + + if !bytes.Equal(expected, got) { + diff := difflib.UnifiedDiff{ + FromFile: "Expected", + A: difflib.SplitLines(string(expected)), + ToFile: "Got", + B: difflib.SplitLines(string(got)), + Context: 3, + } + + text, err := difflib.GetUnifiedDiffString(diff) + c.Assert(err, checker.IsNil) + c.Error(text) + } +} diff --git a/integration/etcd_test.go b/integration/etcd_test.go new file mode 100644 index 000000000..2cafd949a --- /dev/null +++ b/integration/etcd_test.go @@ -0,0 +1,159 @@ +package integration + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/abronan/valkeyrie" + "github.com/abronan/valkeyrie/store" + etcdv3 "github.com/abronan/valkeyrie/store/etcd/v3" + "github.com/containous/traefik/v2/integration/try" + "github.com/containous/traefik/v2/pkg/api" + "github.com/go-check/check" + "github.com/pmezard/go-difflib/difflib" + checker "github.com/vdemeester/shakers" +) + +// etcd test suites (using libcompose) +type EtcdSuite struct { + BaseSuite + kvClient store.Store +} + +func (s *EtcdSuite) setupStore(c *check.C) { + s.createComposeProject(c, "etcd") + s.composeProject.Start(c) + + etcdv3.Register() + kv, err := valkeyrie.NewStore( + store.ETCDV3, + []string{s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress + ":2379"}, + &store.Config{ + ConnectionTimeout: 10 * time.Second, + }, + ) + if err != nil { + c.Fatal("Cannot create store etcd") + } + s.kvClient = kv + + // wait for etcd + err = try.Do(60*time.Second, try.KVExists(kv, "test")) + c.Assert(err, checker.IsNil) +} + +func (s *EtcdSuite) TearDownTest(c *check.C) { + // shutdown and delete compose project + if s.composeProject != nil { + s.composeProject.Stop(c) + } +} + +func (s *EtcdSuite) TearDownSuite(c *check.C) {} + +func (s *EtcdSuite) TestSimpleConfiguration(c *check.C) { + s.setupStore(c) + + address := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress + ":2379" + file := s.adaptFile(c, "fixtures/etcd/simple.toml", struct{ EtcdAddress string }{address}) + defer os.Remove(file) + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + data := map[string]string{ + "traefik/http/routers/Router0/entryPoints/0": "web", + "traefik/http/routers/Router0/middlewares/0": "compressor", + "traefik/http/routers/Router0/middlewares/1": "striper", + "traefik/http/routers/Router0/service": "simplesvc", + "traefik/http/routers/Router0/rule": "Host(`kv1.localhost`)", + "traefik/http/routers/Router0/priority": "42", + "traefik/http/routers/Router0/tls": "", + + "traefik/http/routers/Router1/rule": "Host(`kv2.localhost`)", + "traefik/http/routers/Router1/priority": "42", + "traefik/http/routers/Router1/tls/domains/0/main": "aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/0": "aaa.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/1": "bbb.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/1/main": "bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/0": "aaa.bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/1": "bbb.bbb.localhost", + "traefik/http/routers/Router1/entryPoints/0": "web", + "traefik/http/routers/Router1/service": "simplesvc", + + "traefik/http/services/simplesvc/loadBalancer/servers/0/url": "http://10.0.1.1:8888", + "traefik/http/services/simplesvc/loadBalancer/servers/1/url": "http://10.0.1.1:8889", + + "traefik/http/services/srvcA/loadBalancer/servers/0/url": "http://10.0.1.2:8888", + "traefik/http/services/srvcA/loadBalancer/servers/1/url": "http://10.0.1.2:8889", + + "traefik/http/services/srvcB/loadBalancer/servers/0/url": "http://10.0.1.3:8888", + "traefik/http/services/srvcB/loadBalancer/servers/1/url": "http://10.0.1.3:8889", + + "traefik/http/services/mirror/mirroring/service": "simplesvc", + "traefik/http/services/mirror/mirroring/mirrors/0/name": "srvcA", + "traefik/http/services/mirror/mirroring/mirrors/0/percent": "42", + "traefik/http/services/mirror/mirroring/mirrors/1/name": "srvcB", + "traefik/http/services/mirror/mirroring/mirrors/1/percent": "42", + + "traefik/http/services/Service03/weighted/services/0/name": "srvcA", + "traefik/http/services/Service03/weighted/services/0/weight": "42", + "traefik/http/services/Service03/weighted/services/1/name": "srvcB", + "traefik/http/services/Service03/weighted/services/1/weight": "42", + + "traefik/http/middlewares/compressor/compress": "", + "traefik/http/middlewares/striper/stripPrefix/prefixes/0": "foo", + "traefik/http/middlewares/striper/stripPrefix/prefixes/1": "bar", + "traefik/http/middlewares/striper/stripPrefix/forceSlash": "true", + } + + for k, v := range data { + err := s.kvClient.Put(k, []byte(v), nil) + c.Assert(err, checker.IsNil) + } + + // wait for traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("@etcd")) + c.Assert(err, checker.IsNil) + + resp, err := http.Get("http://127.0.0.1:8080/api/rawdata") + c.Assert(err, checker.IsNil) + + var obtained api.RunTimeRepresentation + err = json.NewDecoder(resp.Body).Decode(&obtained) + c.Assert(err, checker.IsNil) + got, err := json.MarshalIndent(obtained, "", " ") + c.Assert(err, checker.IsNil) + + expectedJSON := filepath.FromSlash("testdata/rawdata-etcd.json") + + if *updateExpected { + err = ioutil.WriteFile(expectedJSON, got, 0666) + c.Assert(err, checker.IsNil) + } + + expected, err := ioutil.ReadFile(expectedJSON) + c.Assert(err, checker.IsNil) + + if !bytes.Equal(expected, got) { + diff := difflib.UnifiedDiff{ + FromFile: "Expected", + A: difflib.SplitLines(string(expected)), + ToFile: "Got", + B: difflib.SplitLines(string(got)), + Context: 3, + } + + text, err := difflib.GetUnifiedDiffString(diff) + c.Assert(err, checker.IsNil) + c.Error(text) + } +} diff --git a/integration/fixtures/consul/simple.toml b/integration/fixtures/consul/simple.toml new file mode 100644 index 000000000..d40f13db1 --- /dev/null +++ b/integration/fixtures/consul/simple.toml @@ -0,0 +1,16 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[providers.consul] + rootKey = "traefik" + endpoints = ["{{ .ConsulAddress }}"] diff --git a/integration/fixtures/etcd/simple.toml b/integration/fixtures/etcd/simple.toml new file mode 100644 index 000000000..f634fff60 --- /dev/null +++ b/integration/fixtures/etcd/simple.toml @@ -0,0 +1,16 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[providers.etcd] + rootKey = "traefik" + endpoints = ["{{ .EtcdAddress }}"] diff --git a/integration/fixtures/redis/simple.toml b/integration/fixtures/redis/simple.toml new file mode 100644 index 000000000..9fcfe670c --- /dev/null +++ b/integration/fixtures/redis/simple.toml @@ -0,0 +1,16 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[providers.redis] + rootKey = "traefik" + endpoints = ["{{ .RedisAddress }}"] diff --git a/integration/fixtures/zookeeper/simple.toml b/integration/fixtures/zookeeper/simple.toml new file mode 100644 index 000000000..3b40dc169 --- /dev/null +++ b/integration/fixtures/zookeeper/simple.toml @@ -0,0 +1,16 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[providers.zookeeper] + rootKey = "traefik" + endpoints = ["{{ .ZkAddress }}"] diff --git a/integration/integration_test.go b/integration/integration_test.go index 130eaeee3..054498c7b 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -36,6 +36,8 @@ func Test(t *testing.T) { // tests launched from a container check.Suite(&AccessLogSuite{}) check.Suite(&AcmeSuite{}) + check.Suite(&EtcdSuite{}) + check.Suite(&ConsulSuite{}) check.Suite(&ConsulCatalogSuite{}) check.Suite(&DockerComposeSuite{}) check.Suite(&DockerSuite{}) @@ -51,6 +53,7 @@ func Test(t *testing.T) { check.Suite(&MarathonSuite{}) check.Suite(&MarathonSuite15{}) check.Suite(&RateLimitSuite{}) + check.Suite(&RedisSuite{}) check.Suite(&RestSuite{}) check.Suite(&RetrySuite{}) check.Suite(&SimpleSuite{}) @@ -58,6 +61,7 @@ func Test(t *testing.T) { check.Suite(&TLSClientHeadersSuite{}) check.Suite(&TracingSuite{}) check.Suite(&WebsocketSuite{}) + check.Suite(&ZookeeperSuite{}) } if *host { // tests launched from the host diff --git a/integration/redis_test.go b/integration/redis_test.go new file mode 100644 index 000000000..b8d1c9c31 --- /dev/null +++ b/integration/redis_test.go @@ -0,0 +1,159 @@ +package integration + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/abronan/valkeyrie" + "github.com/abronan/valkeyrie/store" + "github.com/abronan/valkeyrie/store/redis" + "github.com/containous/traefik/v2/integration/try" + "github.com/containous/traefik/v2/pkg/api" + "github.com/go-check/check" + "github.com/pmezard/go-difflib/difflib" + checker "github.com/vdemeester/shakers" +) + +// Redis test suites (using libcompose) +type RedisSuite struct { + BaseSuite + kvClient store.Store +} + +func (s *RedisSuite) setupStore(c *check.C) { + s.createComposeProject(c, "redis") + s.composeProject.Start(c) + + redis.Register() + kv, err := valkeyrie.NewStore( + store.REDIS, + []string{s.composeProject.Container(c, "redis").NetworkSettings.IPAddress + ":6379"}, + &store.Config{ + ConnectionTimeout: 10 * time.Second, + }, + ) + if err != nil { + c.Fatal("Cannot create store redis") + } + s.kvClient = kv + + // wait for redis + err = try.Do(60*time.Second, try.KVExists(kv, "test")) + c.Assert(err, checker.IsNil) +} + +func (s *RedisSuite) TearDownTest(c *check.C) { + // shutdown and delete compose project + if s.composeProject != nil { + s.composeProject.Stop(c) + } +} + +func (s *RedisSuite) TearDownSuite(c *check.C) {} + +func (s *RedisSuite) TestSimpleConfiguration(c *check.C) { + s.setupStore(c) + + address := s.composeProject.Container(c, "redis").NetworkSettings.IPAddress + ":6379" + file := s.adaptFile(c, "fixtures/redis/simple.toml", struct{ RedisAddress string }{address}) + defer os.Remove(file) + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + data := map[string]string{ + "traefik/http/routers/Router0/entryPoints/0": "web", + "traefik/http/routers/Router0/middlewares/0": "compressor", + "traefik/http/routers/Router0/middlewares/1": "striper", + "traefik/http/routers/Router0/service": "simplesvc", + "traefik/http/routers/Router0/rule": "Host(`kv1.localhost`)", + "traefik/http/routers/Router0/priority": "42", + "traefik/http/routers/Router0/tls": "", + + "traefik/http/routers/Router1/rule": "Host(`kv2.localhost`)", + "traefik/http/routers/Router1/priority": "42", + "traefik/http/routers/Router1/tls/domains/0/main": "aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/0": "aaa.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/1": "bbb.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/1/main": "bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/0": "aaa.bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/1": "bbb.bbb.localhost", + "traefik/http/routers/Router1/entryPoints/0": "web", + "traefik/http/routers/Router1/service": "simplesvc", + + "traefik/http/services/simplesvc/loadBalancer/servers/0/url": "http://10.0.1.1:8888", + "traefik/http/services/simplesvc/loadBalancer/servers/1/url": "http://10.0.1.1:8889", + + "traefik/http/services/srvcA/loadBalancer/servers/0/url": "http://10.0.1.2:8888", + "traefik/http/services/srvcA/loadBalancer/servers/1/url": "http://10.0.1.2:8889", + + "traefik/http/services/srvcB/loadBalancer/servers/0/url": "http://10.0.1.3:8888", + "traefik/http/services/srvcB/loadBalancer/servers/1/url": "http://10.0.1.3:8889", + + "traefik/http/services/mirror/mirroring/service": "simplesvc", + "traefik/http/services/mirror/mirroring/mirrors/0/name": "srvcA", + "traefik/http/services/mirror/mirroring/mirrors/0/percent": "42", + "traefik/http/services/mirror/mirroring/mirrors/1/name": "srvcB", + "traefik/http/services/mirror/mirroring/mirrors/1/percent": "42", + + "traefik/http/services/Service03/weighted/services/0/name": "srvcA", + "traefik/http/services/Service03/weighted/services/0/weight": "42", + "traefik/http/services/Service03/weighted/services/1/name": "srvcB", + "traefik/http/services/Service03/weighted/services/1/weight": "42", + + "traefik/http/middlewares/compressor/compress": "", + "traefik/http/middlewares/striper/stripPrefix/prefixes/0": "foo", + "traefik/http/middlewares/striper/stripPrefix/prefixes/1": "bar", + "traefik/http/middlewares/striper/stripPrefix/forceSlash": "true", + } + + for k, v := range data { + err := s.kvClient.Put(k, []byte(v), nil) + c.Assert(err, checker.IsNil) + } + + // wait for traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("@redis")) + c.Assert(err, checker.IsNil) + + resp, err := http.Get("http://127.0.0.1:8080/api/rawdata") + c.Assert(err, checker.IsNil) + + var obtained api.RunTimeRepresentation + err = json.NewDecoder(resp.Body).Decode(&obtained) + c.Assert(err, checker.IsNil) + got, err := json.MarshalIndent(obtained, "", " ") + c.Assert(err, checker.IsNil) + + expectedJSON := filepath.FromSlash("testdata/rawdata-redis.json") + + if *updateExpected { + err = ioutil.WriteFile(expectedJSON, got, 0666) + c.Assert(err, checker.IsNil) + } + + expected, err := ioutil.ReadFile(expectedJSON) + c.Assert(err, checker.IsNil) + + if !bytes.Equal(expected, got) { + diff := difflib.UnifiedDiff{ + FromFile: "Expected", + A: difflib.SplitLines(string(expected)), + ToFile: "Got", + B: difflib.SplitLines(string(got)), + Context: 3, + } + + text, err := difflib.GetUnifiedDiffString(diff) + c.Assert(err, checker.IsNil) + c.Error(text) + } +} diff --git a/integration/resources/compose/consul.yml b/integration/resources/compose/consul.yml new file mode 100644 index 000000000..507272053 --- /dev/null +++ b/integration/resources/compose/consul.yml @@ -0,0 +1,4 @@ +consul: + image: consul:1.6 + ports: + - "8500:8500" diff --git a/integration/resources/compose/etcd.yml b/integration/resources/compose/etcd.yml new file mode 100644 index 000000000..2763a9c77 --- /dev/null +++ b/integration/resources/compose/etcd.yml @@ -0,0 +1,5 @@ +etcd: + image: quay.io/coreos/etcd:v3.3.18 + command: etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2380 + ports: + - "2379:2379" diff --git a/integration/resources/compose/redis.yml b/integration/resources/compose/redis.yml new file mode 100644 index 000000000..224e27087 --- /dev/null +++ b/integration/resources/compose/redis.yml @@ -0,0 +1,4 @@ +redis: + image: redis:5.0 + ports: + - "6379:6379" diff --git a/integration/resources/compose/zookeeper.yml b/integration/resources/compose/zookeeper.yml new file mode 100644 index 000000000..ef0d245c5 --- /dev/null +++ b/integration/resources/compose/zookeeper.yml @@ -0,0 +1,4 @@ +zookeeper: + image: zookeeper:3.5 + ports: + - "2181:2181" diff --git a/integration/testdata/rawdata-consul.json b/integration/testdata/rawdata-consul.json new file mode 100644 index 000000000..2f42deca9 --- /dev/null +++ b/integration/testdata/rawdata-consul.json @@ -0,0 +1,219 @@ +{ + "routers": { + "Router0@consul": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "compressor@consul", + "striper@consul" + ], + "service": "simplesvc", + "rule": "Host(`kv1.localhost`)", + "priority": 42, + "tls": {}, + "status": "enabled", + "using": [ + "web" + ] + }, + "Router1@consul": { + "entryPoints": [ + "web" + ], + "service": "simplesvc", + "rule": "Host(`kv2.localhost`)", + "priority": 42, + "tls": { + "domains": [ + { + "main": "aaa.localhost", + "sans": [ + "aaa.aaa.localhost", + "bbb.aaa.localhost" + ] + }, + { + "main": "bbb.localhost", + "sans": [ + "aaa.bbb.localhost", + "bbb.bbb.localhost" + ] + } + ] + }, + "status": "enabled", + "using": [ + "web" + ] + }, + "api@internal": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "priority": 2147483646, + "status": "enabled", + "using": [ + "traefik" + ] + }, + "dashboard@internal": { + "entryPoints": [ + "traefik" + ], + "middlewares": [ + "dashboard_redirect@internal", + "dashboard_stripprefix@internal" + ], + "service": "dashboard@internal", + "rule": "PathPrefix(`/`)", + "priority": 2147483645, + "status": "enabled", + "using": [ + "traefik" + ] + } + }, + "middlewares": { + "compressor@consul": { + "compress": {}, + "status": "enabled", + "usedBy": [ + "Router0@consul" + ] + }, + "dashboard_redirect@internal": { + "redirectRegex": { + "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "replacement": "${1}/dashboard/", + "permanent": true + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "dashboard_stripprefix@internal": { + "stripPrefix": { + "prefixes": [ + "/dashboard/", + "/dashboard" + ] + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "striper@consul": { + "stripPrefix": { + "prefixes": [ + "foo", + "bar" + ], + "forceSlash": true + }, + "status": "enabled", + "usedBy": [ + "Router0@consul" + ] + } + }, + "services": { + "Service03@consul": { + "weighted": { + "services": [ + { + "name": "srvcA", + "weight": 42 + }, + { + "name": "srvcB", + "weight": 42 + } + ] + }, + "status": "enabled" + }, + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "mirror@consul": { + "mirroring": { + "service": "simplesvc", + "mirrors": [ + { + "name": "srvcA", + "percent": 42 + }, + { + "name": "srvcB", + "percent": 42 + } + ] + }, + "status": "enabled" + }, + "simplesvc@consul": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.1:8888" + }, + { + "url": "http://10.0.1.1:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "Router0@consul", + "Router1@consul" + ], + "serverStatus": { + "http://10.0.1.1:8888": "UP", + "http://10.0.1.1:8889": "UP" + } + }, + "srvcA@consul": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.2:8888" + }, + { + "url": "http://10.0.1.2:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + }, + "srvcB@consul": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.3:8888" + }, + { + "url": "http://10.0.1.3:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + } + } +} \ No newline at end of file diff --git a/integration/testdata/rawdata-etcd.json b/integration/testdata/rawdata-etcd.json new file mode 100644 index 000000000..dac6c6ae4 --- /dev/null +++ b/integration/testdata/rawdata-etcd.json @@ -0,0 +1,219 @@ +{ + "routers": { + "Router0@etcd": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "compressor@etcd", + "striper@etcd" + ], + "service": "simplesvc", + "rule": "Host(`kv1.localhost`)", + "priority": 42, + "tls": {}, + "status": "enabled", + "using": [ + "web" + ] + }, + "Router1@etcd": { + "entryPoints": [ + "web" + ], + "service": "simplesvc", + "rule": "Host(`kv2.localhost`)", + "priority": 42, + "tls": { + "domains": [ + { + "main": "aaa.localhost", + "sans": [ + "aaa.aaa.localhost", + "bbb.aaa.localhost" + ] + }, + { + "main": "bbb.localhost", + "sans": [ + "aaa.bbb.localhost", + "bbb.bbb.localhost" + ] + } + ] + }, + "status": "enabled", + "using": [ + "web" + ] + }, + "api@internal": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "priority": 2147483646, + "status": "enabled", + "using": [ + "traefik" + ] + }, + "dashboard@internal": { + "entryPoints": [ + "traefik" + ], + "middlewares": [ + "dashboard_redirect@internal", + "dashboard_stripprefix@internal" + ], + "service": "dashboard@internal", + "rule": "PathPrefix(`/`)", + "priority": 2147483645, + "status": "enabled", + "using": [ + "traefik" + ] + } + }, + "middlewares": { + "compressor@etcd": { + "compress": {}, + "status": "enabled", + "usedBy": [ + "Router0@etcd" + ] + }, + "dashboard_redirect@internal": { + "redirectRegex": { + "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "replacement": "${1}/dashboard/", + "permanent": true + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "dashboard_stripprefix@internal": { + "stripPrefix": { + "prefixes": [ + "/dashboard/", + "/dashboard" + ] + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "striper@etcd": { + "stripPrefix": { + "prefixes": [ + "foo", + "bar" + ], + "forceSlash": true + }, + "status": "enabled", + "usedBy": [ + "Router0@etcd" + ] + } + }, + "services": { + "Service03@etcd": { + "weighted": { + "services": [ + { + "name": "srvcA", + "weight": 42 + }, + { + "name": "srvcB", + "weight": 42 + } + ] + }, + "status": "enabled" + }, + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "mirror@etcd": { + "mirroring": { + "service": "simplesvc", + "mirrors": [ + { + "name": "srvcA", + "percent": 42 + }, + { + "name": "srvcB", + "percent": 42 + } + ] + }, + "status": "enabled" + }, + "simplesvc@etcd": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.1:8888" + }, + { + "url": "http://10.0.1.1:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "Router0@etcd", + "Router1@etcd" + ], + "serverStatus": { + "http://10.0.1.1:8888": "UP", + "http://10.0.1.1:8889": "UP" + } + }, + "srvcA@etcd": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.2:8888" + }, + { + "url": "http://10.0.1.2:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + }, + "srvcB@etcd": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.3:8888" + }, + { + "url": "http://10.0.1.3:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + } + } +} \ No newline at end of file diff --git a/integration/testdata/rawdata-redis.json b/integration/testdata/rawdata-redis.json new file mode 100644 index 000000000..bb4eb10a0 --- /dev/null +++ b/integration/testdata/rawdata-redis.json @@ -0,0 +1,219 @@ +{ + "routers": { + "Router0@redis": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "compressor@redis", + "striper@redis" + ], + "service": "simplesvc", + "rule": "Host(`kv1.localhost`)", + "priority": 42, + "tls": {}, + "status": "enabled", + "using": [ + "web" + ] + }, + "Router1@redis": { + "entryPoints": [ + "web" + ], + "service": "simplesvc", + "rule": "Host(`kv2.localhost`)", + "priority": 42, + "tls": { + "domains": [ + { + "main": "aaa.localhost", + "sans": [ + "aaa.aaa.localhost", + "bbb.aaa.localhost" + ] + }, + { + "main": "bbb.localhost", + "sans": [ + "aaa.bbb.localhost", + "bbb.bbb.localhost" + ] + } + ] + }, + "status": "enabled", + "using": [ + "web" + ] + }, + "api@internal": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "priority": 2147483646, + "status": "enabled", + "using": [ + "traefik" + ] + }, + "dashboard@internal": { + "entryPoints": [ + "traefik" + ], + "middlewares": [ + "dashboard_redirect@internal", + "dashboard_stripprefix@internal" + ], + "service": "dashboard@internal", + "rule": "PathPrefix(`/`)", + "priority": 2147483645, + "status": "enabled", + "using": [ + "traefik" + ] + } + }, + "middlewares": { + "compressor@redis": { + "compress": {}, + "status": "enabled", + "usedBy": [ + "Router0@redis" + ] + }, + "dashboard_redirect@internal": { + "redirectRegex": { + "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "replacement": "${1}/dashboard/", + "permanent": true + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "dashboard_stripprefix@internal": { + "stripPrefix": { + "prefixes": [ + "/dashboard/", + "/dashboard" + ] + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "striper@redis": { + "stripPrefix": { + "prefixes": [ + "foo", + "bar" + ], + "forceSlash": true + }, + "status": "enabled", + "usedBy": [ + "Router0@redis" + ] + } + }, + "services": { + "Service03@redis": { + "weighted": { + "services": [ + { + "name": "srvcA", + "weight": 42 + }, + { + "name": "srvcB", + "weight": 42 + } + ] + }, + "status": "enabled" + }, + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "mirror@redis": { + "mirroring": { + "service": "simplesvc", + "mirrors": [ + { + "name": "srvcA", + "percent": 42 + }, + { + "name": "srvcB", + "percent": 42 + } + ] + }, + "status": "enabled" + }, + "simplesvc@redis": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.1:8888" + }, + { + "url": "http://10.0.1.1:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "Router0@redis", + "Router1@redis" + ], + "serverStatus": { + "http://10.0.1.1:8888": "UP", + "http://10.0.1.1:8889": "UP" + } + }, + "srvcA@redis": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.2:8888" + }, + { + "url": "http://10.0.1.2:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + }, + "srvcB@redis": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.3:8888" + }, + { + "url": "http://10.0.1.3:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + } + } +} \ No newline at end of file diff --git a/integration/testdata/rawdata-zk.json b/integration/testdata/rawdata-zk.json new file mode 100644 index 000000000..616b8e7c4 --- /dev/null +++ b/integration/testdata/rawdata-zk.json @@ -0,0 +1,219 @@ +{ + "routers": { + "Router0@zookeeper": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "compressor@zookeeper", + "striper@zookeeper" + ], + "service": "simplesvc", + "rule": "Host(`kv1.localhost`)", + "priority": 42, + "tls": {}, + "status": "enabled", + "using": [ + "web" + ] + }, + "Router1@zookeeper": { + "entryPoints": [ + "web" + ], + "service": "simplesvc", + "rule": "Host(`kv2.localhost`)", + "priority": 42, + "tls": { + "domains": [ + { + "main": "aaa.localhost", + "sans": [ + "aaa.aaa.localhost", + "bbb.aaa.localhost" + ] + }, + { + "main": "bbb.localhost", + "sans": [ + "aaa.bbb.localhost", + "bbb.bbb.localhost" + ] + } + ] + }, + "status": "enabled", + "using": [ + "web" + ] + }, + "api@internal": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "priority": 2147483646, + "status": "enabled", + "using": [ + "traefik" + ] + }, + "dashboard@internal": { + "entryPoints": [ + "traefik" + ], + "middlewares": [ + "dashboard_redirect@internal", + "dashboard_stripprefix@internal" + ], + "service": "dashboard@internal", + "rule": "PathPrefix(`/`)", + "priority": 2147483645, + "status": "enabled", + "using": [ + "traefik" + ] + } + }, + "middlewares": { + "compressor@zookeeper": { + "compress": {}, + "status": "enabled", + "usedBy": [ + "Router0@zookeeper" + ] + }, + "dashboard_redirect@internal": { + "redirectRegex": { + "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", + "replacement": "${1}/dashboard/", + "permanent": true + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "dashboard_stripprefix@internal": { + "stripPrefix": { + "prefixes": [ + "/dashboard/", + "/dashboard" + ] + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "striper@zookeeper": { + "stripPrefix": { + "prefixes": [ + "foo", + "bar" + ], + "forceSlash": true + }, + "status": "enabled", + "usedBy": [ + "Router0@zookeeper" + ] + } + }, + "services": { + "Service03@zookeeper": { + "weighted": { + "services": [ + { + "name": "srvcA", + "weight": 42 + }, + { + "name": "srvcB", + "weight": 42 + } + ] + }, + "status": "enabled" + }, + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "mirror@zookeeper": { + "mirroring": { + "service": "simplesvc", + "mirrors": [ + { + "name": "srvcA", + "percent": 42 + }, + { + "name": "srvcB", + "percent": 42 + } + ] + }, + "status": "enabled" + }, + "simplesvc@zookeeper": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.1:8888" + }, + { + "url": "http://10.0.1.1:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "Router0@zookeeper", + "Router1@zookeeper" + ], + "serverStatus": { + "http://10.0.1.1:8888": "UP", + "http://10.0.1.1:8889": "UP" + } + }, + "srvcA@zookeeper": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.2:8888" + }, + { + "url": "http://10.0.1.2:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + }, + "srvcB@zookeeper": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.0.1.3:8888" + }, + { + "url": "http://10.0.1.3:8889" + } + ], + "passHostHeader": true + }, + "status": "enabled" + } + } +} \ No newline at end of file diff --git a/integration/zk_test.go b/integration/zk_test.go new file mode 100644 index 000000000..a9544b22d --- /dev/null +++ b/integration/zk_test.go @@ -0,0 +1,159 @@ +package integration + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/abronan/valkeyrie" + "github.com/abronan/valkeyrie/store" + "github.com/abronan/valkeyrie/store/zookeeper" + "github.com/containous/traefik/v2/integration/try" + "github.com/containous/traefik/v2/pkg/api" + "github.com/go-check/check" + "github.com/pmezard/go-difflib/difflib" + checker "github.com/vdemeester/shakers" +) + +// Zk test suites (using libcompose) +type ZookeeperSuite struct { + BaseSuite + kvClient store.Store +} + +func (s *ZookeeperSuite) setupStore(c *check.C) { + s.createComposeProject(c, "zookeeper") + s.composeProject.Start(c) + + zookeeper.Register() + kv, err := valkeyrie.NewStore( + store.ZK, + []string{s.composeProject.Container(c, "zookeeper").NetworkSettings.IPAddress + ":2181"}, + &store.Config{ + ConnectionTimeout: 10 * time.Second, + }, + ) + if err != nil { + c.Fatal("Cannot create store zookeeper") + } + s.kvClient = kv + + // wait for zk + err = try.Do(60*time.Second, try.KVExists(kv, "test")) + c.Assert(err, checker.IsNil) +} + +func (s *ZookeeperSuite) TearDownTest(c *check.C) { + // shutdown and delete compose project + if s.composeProject != nil { + s.composeProject.Stop(c) + } +} + +func (s *ZookeeperSuite) TearDownSuite(c *check.C) {} + +func (s *ZookeeperSuite) TestSimpleConfiguration(c *check.C) { + s.setupStore(c) + + address := s.composeProject.Container(c, "zookeeper").NetworkSettings.IPAddress + ":2181" + file := s.adaptFile(c, "fixtures/zookeeper/simple.toml", struct{ ZkAddress string }{address}) + defer os.Remove(file) + + data := map[string]string{ + "traefik/http/routers/Router0/entryPoints/0": "web", + "traefik/http/routers/Router0/middlewares/0": "compressor", + "traefik/http/routers/Router0/middlewares/1": "striper", + "traefik/http/routers/Router0/service": "simplesvc", + "traefik/http/routers/Router0/rule": "Host(`kv1.localhost`)", + "traefik/http/routers/Router0/priority": "42", + "traefik/http/routers/Router0/tls": "", + + "traefik/http/routers/Router1/rule": "Host(`kv2.localhost`)", + "traefik/http/routers/Router1/priority": "42", + "traefik/http/routers/Router1/tls/domains/0/main": "aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/0": "aaa.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/0/sans/1": "bbb.aaa.localhost", + "traefik/http/routers/Router1/tls/domains/1/main": "bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/0": "aaa.bbb.localhost", + "traefik/http/routers/Router1/tls/domains/1/sans/1": "bbb.bbb.localhost", + "traefik/http/routers/Router1/entryPoints/0": "web", + "traefik/http/routers/Router1/service": "simplesvc", + + "traefik/http/services/simplesvc/loadBalancer/servers/0/url": "http://10.0.1.1:8888", + "traefik/http/services/simplesvc/loadBalancer/servers/1/url": "http://10.0.1.1:8889", + + "traefik/http/services/srvcA/loadBalancer/servers/0/url": "http://10.0.1.2:8888", + "traefik/http/services/srvcA/loadBalancer/servers/1/url": "http://10.0.1.2:8889", + + "traefik/http/services/srvcB/loadBalancer/servers/0/url": "http://10.0.1.3:8888", + "traefik/http/services/srvcB/loadBalancer/servers/1/url": "http://10.0.1.3:8889", + + "traefik/http/services/mirror/mirroring/service": "simplesvc", + "traefik/http/services/mirror/mirroring/mirrors/0/name": "srvcA", + "traefik/http/services/mirror/mirroring/mirrors/0/percent": "42", + "traefik/http/services/mirror/mirroring/mirrors/1/name": "srvcB", + "traefik/http/services/mirror/mirroring/mirrors/1/percent": "42", + + "traefik/http/services/Service03/weighted/services/0/name": "srvcA", + "traefik/http/services/Service03/weighted/services/0/weight": "42", + "traefik/http/services/Service03/weighted/services/1/name": "srvcB", + "traefik/http/services/Service03/weighted/services/1/weight": "42", + + "traefik/http/middlewares/compressor/compress": "", + "traefik/http/middlewares/striper/stripPrefix/prefixes/0": "foo", + "traefik/http/middlewares/striper/stripPrefix/prefixes/1": "bar", + "traefik/http/middlewares/striper/stripPrefix/forceSlash": "true", + } + + for k, v := range data { + err := s.kvClient.Put(k, []byte(v), nil) + c.Assert(err, checker.IsNil) + } + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("@zookeeper")) + c.Assert(err, checker.IsNil) + + resp, err := http.Get("http://127.0.0.1:8080/api/rawdata") + c.Assert(err, checker.IsNil) + + var obtained api.RunTimeRepresentation + err = json.NewDecoder(resp.Body).Decode(&obtained) + c.Assert(err, checker.IsNil) + got, err := json.MarshalIndent(obtained, "", " ") + c.Assert(err, checker.IsNil) + + expectedJSON := filepath.FromSlash("testdata/rawdata-zk.json") + + if *updateExpected { + err = ioutil.WriteFile(expectedJSON, got, 0666) + c.Assert(err, checker.IsNil) + } + + expected, err := ioutil.ReadFile(expectedJSON) + c.Assert(err, checker.IsNil) + + if !bytes.Equal(expected, got) { + diff := difflib.UnifiedDiff{ + FromFile: "Expected", + A: difflib.SplitLines(string(expected)), + ToFile: "Got", + B: difflib.SplitLines(string(got)), + Context: 3, + } + + text, err := difflib.GetUnifiedDiffString(diff) + c.Assert(err, checker.IsNil) + c.Error(text) + } +} diff --git a/internal/gendoc.go b/internal/gendoc.go index 1f102082a..505e98192 100644 --- a/internal/gendoc.go +++ b/internal/gendoc.go @@ -4,8 +4,12 @@ import ( "fmt" "io" "os" + "path" + "sort" + "strconv" "strings" + "github.com/BurntSushi/toml" "github.com/containous/traefik/v2/pkg/config/env" "github.com/containous/traefik/v2/pkg/config/flag" "github.com/containous/traefik/v2/pkg/config/generator" @@ -17,6 +21,7 @@ import ( func main() { genStaticConfDoc("./docs/content/reference/static-configuration/env-ref.md", "", env.Encode) genStaticConfDoc("./docs/content/reference/static-configuration/cli-ref.md", "--", flag.Encode) + genKVDynConfDoc("./docs/content/reference/dynamic-configuration/kv-ref.md") } func genStaticConfDoc(outputFile string, prefix string, encodeFn func(interface{}) ([]parser.Flat, error)) { @@ -81,3 +86,94 @@ func (ew *errWriter) writeln(a ...interface{}) { _, ew.err = fmt.Fprintln(ew.w, a...) } + +func genKVDynConfDoc(outputFile string) { + dynConfPath := "./docs/content/reference/dynamic-configuration/file.toml" + conf := map[string]interface{}{} + _, err := toml.DecodeFile(dynConfPath, &conf) + if err != nil { + log.Fatal(err) + } + + file, err := os.Create(outputFile) + if err != nil { + log.Fatal(err) + } + + store := storeWriter{data: map[string]string{}} + c := client{store: store} + err = c.load("traefik", conf) + if err != nil { + log.Fatal(err) + } + + var keys []string + for k := range store.data { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + _, _ = fmt.Fprintf(file, "| `%s` | `%s` |\n", k, store.data[k]) + } +} + +type storeWriter struct { + data map[string]string +} + +func (f storeWriter) Put(key string, value []byte, options []string) error { + f.data[key] = string(value) + return nil +} + +type client struct { + store storeWriter +} + +func (c client) load(parentKey string, conf map[string]interface{}) error { + for k, v := range conf { + switch entry := v.(type) { + case map[string]interface{}: + key := path.Join(parentKey, k) + + if len(entry) == 0 { + err := c.store.Put(key, nil, nil) + if err != nil { + return err + } + } else { + err := c.load(key, entry) + if err != nil { + return err + } + } + case []map[string]interface{}: + for i, o := range entry { + key := path.Join(parentKey, k, strconv.Itoa(i)) + + if err := c.load(key, o); err != nil { + return err + } + } + case []interface{}: + for i, o := range entry { + key := path.Join(parentKey, k, strconv.Itoa(i)) + + err := c.store.Put(key, []byte(fmt.Sprintf("%v", o)), nil) + if err != nil { + return err + } + } + default: + key := path.Join(parentKey, k) + + err := c.store.Put(key, []byte(fmt.Sprintf("%v", v)), nil) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/config/env/env.go b/pkg/config/env/env.go index 5fbd2a877..4fc5071e8 100644 --- a/pkg/config/env/env.go +++ b/pkg/config/env/env.go @@ -46,17 +46,20 @@ func Encode(element interface{}) ([]parser.Flat, error) { return nil, nil } - node, err := parser.EncodeToNode(element, parser.DefaultRootName, false) + etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true} + node, err := parser.EncodeToNode(element, parser.DefaultRootName, etnOpts) if err != nil { return nil, err } - err = parser.AddMetadata(element, node) + metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true} + err = parser.AddMetadata(element, node, metaOpts) if err != nil { return nil, err } - return parser.EncodeToFlat(element, node, parser.FlatOpts{Case: "upper", Separator: "_"}) + flatOpts := parser.FlatOpts{Case: "upper", Separator: "_", TagName: parser.TagLabel} + return parser.EncodeToFlat(element, node, flatOpts) } func checkPrefix(prefix string) error { diff --git a/pkg/config/file/file.go b/pkg/config/file/file.go index 2c0ca10e0..619320307 100644 --- a/pkg/config/file/file.go +++ b/pkg/config/file/file.go @@ -22,10 +22,11 @@ func Decode(filePath string, element interface{}) error { return err } - err = parser.AddMetadata(element, root) + metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true} + err = parser.AddMetadata(element, root, metaOpts) if err != nil { return err } - return parser.Fill(element, root) + return parser.Fill(element, root, parser.FillerOpts{AllowSliceAsStruct: true}) } diff --git a/pkg/config/flag/flag.go b/pkg/config/flag/flag.go index 354335647..437744f18 100644 --- a/pkg/config/flag/flag.go +++ b/pkg/config/flag/flag.go @@ -30,15 +30,18 @@ func Encode(element interface{}) ([]parser.Flat, error) { return nil, nil } - node, err := parser.EncodeToNode(element, parser.DefaultRootName, false) + etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true} + node, err := parser.EncodeToNode(element, parser.DefaultRootName, etnOpts) if err != nil { return nil, err } - err = parser.AddMetadata(element, node) + metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true} + err = parser.AddMetadata(element, node, metaOpts) if err != nil { return nil, err } - return parser.EncodeToFlat(element, node, parser.FlatOpts{Separator: ".", SkipRoot: true}) + flatOpts := parser.FlatOpts{Separator: ".", SkipRoot: true, TagName: parser.TagLabel} + return parser.EncodeToFlat(element, node, flatOpts) } diff --git a/pkg/config/kv/kv.go b/pkg/config/kv/kv.go new file mode 100644 index 000000000..940e84ff4 --- /dev/null +++ b/pkg/config/kv/kv.go @@ -0,0 +1,75 @@ +package kv + +import ( + "path" + "reflect" + + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/config/parser" +) + +// Decode decodes the given KV pairs into the given element. +// The operation goes through three stages roughly summarized as: +// KV pairs -> tree of untyped nodes +// untyped nodes -> nodes augmented with metadata such as kind (inferred from element) +// "typed" nodes -> typed element +func Decode(pairs []*store.KVPair, element interface{}, rootName string) error { + if element == nil { + return nil + } + + filters := getRootFieldNames(rootName, element) + + node, err := DecodeToNode(pairs, rootName, filters...) + if err != nil { + return err + } + + metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: false} + err = parser.AddMetadata(element, node, metaOpts) + if err != nil { + return err + } + + return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: false}) +} + +func getRootFieldNames(rootName string, element interface{}) []string { + if element == nil { + return nil + } + + rootType := reflect.TypeOf(element) + + return getFieldNames(rootName, rootType) +} + +func getFieldNames(rootName string, rootType reflect.Type) []string { + var names []string + + if rootType.Kind() == reflect.Ptr { + rootType = rootType.Elem() + } + + if rootType.Kind() != reflect.Struct { + return nil + } + + for i := 0; i < rootType.NumField(); i++ { + field := rootType.Field(i) + + if !parser.IsExported(field) { + continue + } + + if field.Anonymous && + (field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) { + names = append(names, getFieldNames(rootName, field.Type)...) + continue + } + + names = append(names, path.Join(rootName, field.Name)) + } + + return names +} diff --git a/pkg/config/kv/kv_node.go b/pkg/config/kv/kv_node.go new file mode 100644 index 000000000..e9178d4bc --- /dev/null +++ b/pkg/config/kv/kv_node.go @@ -0,0 +1,128 @@ +package kv + +import ( + "fmt" + "regexp" + "sort" + "strings" + + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/config/parser" +) + +// DecodeToNode converts the labels to a tree of nodes. +// If any filters are present, labels which do not match the filters are skipped. +func DecodeToNode(pairs []*store.KVPair, rootName string, filters ...string) (*parser.Node, error) { + sortedPairs := filterPairs(pairs, filters) + + exp := regexp.MustCompile(`^\d+$`) + + var node *parser.Node + + for i, pair := range sortedPairs { + split := strings.FieldsFunc(pair.Key, func(c rune) bool { return c == '/' }) + + if split[0] != rootName { + return nil, fmt.Errorf("invalid label root %s", split[0]) + } + + var parts []string + for _, fragment := range split { + if exp.MatchString(fragment) { + parts = append(parts, "["+fragment+"]") + } else { + parts = append(parts, fragment) + } + } + + if i == 0 { + node = &parser.Node{} + } + decodeToNode(node, parts, string(pair.Value)) + } + + return node, nil +} + +func decodeToNode(root *parser.Node, path []string, value string) { + if len(root.Name) == 0 { + root.Name = path[0] + } + + // it's a leaf or not -> children + if len(path) > 1 { + if n := containsNode(root.Children, path[1]); n != nil { + // the child already exists + decodeToNode(n, path[1:], value) + } else { + // new child + child := &parser.Node{Name: path[1]} + decodeToNode(child, path[1:], value) + root.Children = append(root.Children, child) + } + } else { + root.Value = value + } +} + +func containsNode(nodes []*parser.Node, name string) *parser.Node { + for _, n := range nodes { + if strings.EqualFold(name, n.Name) { + return n + } + } + return nil +} + +func filterPairs(pairs []*store.KVPair, filters []string) []*store.KVPair { + exp := regexp.MustCompile(`^(.+)/\d+$`) + + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Key < pairs[j].Key + }) + + var simplePairs = map[string]*store.KVPair{} + var slicePairs = map[string][]string{} + + for _, pair := range pairs { + if len(filters) == 0 { + // Slice of simple type + if exp.MatchString(pair.Key) { + sanitizedKey := exp.FindStringSubmatch(pair.Key)[1] + slicePairs[sanitizedKey] = append(slicePairs[sanitizedKey], string(pair.Value)) + } else { + simplePairs[pair.Key] = pair + } + continue + } + + for _, filter := range filters { + if len(pair.Key) >= len(filter) && strings.EqualFold(pair.Key[:len(filter)], filter) { + // Slice of simple type + if exp.MatchString(pair.Key) { + sanitizedKey := exp.FindStringSubmatch(pair.Key)[1] + slicePairs[sanitizedKey] = append(slicePairs[sanitizedKey], string(pair.Value)) + } else { + simplePairs[pair.Key] = pair + } + continue + } + } + } + + var sortedPairs []*store.KVPair + for k, v := range slicePairs { + delete(simplePairs, k) + sortedPairs = append(sortedPairs, &store.KVPair{Key: k, Value: []byte(strings.Join(v, ","))}) + } + + for _, v := range simplePairs { + sortedPairs = append(sortedPairs, v) + } + + sort.Slice(sortedPairs, func(i, j int) bool { + return sortedPairs[i].Key < sortedPairs[j].Key + }) + + return sortedPairs +} diff --git a/pkg/config/kv/kv_node_test.go b/pkg/config/kv/kv_node_test.go new file mode 100644 index 000000000..7125a4467 --- /dev/null +++ b/pkg/config/kv/kv_node_test.go @@ -0,0 +1,274 @@ +package kv + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/config/parser" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecodeToNode(t *testing.T) { + type expected struct { + error bool + node *parser.Node + } + + testCases := []struct { + desc string + in map[string]string + filters []string + expected expected + }{ + { + desc: "no label", + in: map[string]string{}, + expected: expected{node: nil}, + }, + { + desc: "level 1", + in: map[string]string{ + "traefik/foo": "bar", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Value: "bar"}, + }, + }}, + }, + { + desc: "level 1 empty value", + in: map[string]string{ + "traefik/foo": "", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Value: ""}, + }, + }}, + }, + { + desc: "level 2", + in: map[string]string{ + "traefik/foo/bar": "bar", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{{ + Name: "foo", + Children: []*parser.Node{ + {Name: "bar", Value: "bar"}, + }, + }}, + }}, + }, + { + desc: "several entries, level 0", + in: map[string]string{ + "traefik": "bar", + "traefic": "bur", + }, + expected: expected{error: true}, + }, + { + desc: "several entries, prefix filter", + in: map[string]string{ + "traefik/foo": "bar", + "traefik/fii": "bir", + }, + filters: []string{"traefik/Foo"}, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Value: "bar"}, + }, + }}, + }, + { + desc: "several entries, level 1", + in: map[string]string{ + "traefik/foo": "bar", + "traefik/fii": "bur", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "fii", Value: "bur"}, + {Name: "foo", Value: "bar"}, + }, + }}, + }, + { + desc: "several entries, level 2", + in: map[string]string{ + "traefik/foo/aaa": "bar", + "traefik/foo/bbb": "bur", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Children: []*parser.Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }, + }}, + }, + { + desc: "several entries, level 2, case insensitive", + in: map[string]string{ + "traefik/foo/aaa": "bar", + "traefik/Foo/bbb": "bur", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "Foo", Children: []*parser.Node{ + {Name: "bbb", Value: "bur"}, + {Name: "aaa", Value: "bar"}, + }}, + }, + }}, + }, + { + desc: "several entries, level 2, 3 children", + in: map[string]string{ + "traefik/foo/aaa": "bar", + "traefik/foo/bbb": "bur", + "traefik/foo/ccc": "bir", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Children: []*parser.Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + {Name: "ccc", Value: "bir"}, + }}, + }, + }}, + }, + { + desc: "several entries, level 3", + in: map[string]string{ + "traefik/foo/bar/aaa": "bar", + "traefik/foo/bar/bbb": "bur", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Children: []*parser.Node{ + {Name: "bar", Children: []*parser.Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }}, + }, + }}, + }, + { + desc: "several entries, level 3, 2 children level 1", + in: map[string]string{ + "traefik/foo/bar/aaa": "bar", + "traefik/foo/bar/bbb": "bur", + "traefik/bar/foo/bbb": "bir", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "bar", Children: []*parser.Node{ + {Name: "foo", Children: []*parser.Node{ + {Name: "bbb", Value: "bir"}, + }}, + }}, + {Name: "foo", Children: []*parser.Node{ + {Name: "bar", Children: []*parser.Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }}, + }, + }}, + }, + { + desc: "several entries, slice syntax", + in: map[string]string{ + "traefik/foo/0/aaa": "bar0", + "traefik/foo/0/bbb": "bur0", + "traefik/foo/1/aaa": "bar1", + "traefik/foo/1/bbb": "bur1", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Children: []*parser.Node{ + {Name: "[0]", Children: []*parser.Node{ + {Name: "aaa", Value: "bar0"}, + {Name: "bbb", Value: "bur0"}, + }}, + {Name: "[1]", Children: []*parser.Node{ + {Name: "aaa", Value: "bar1"}, + {Name: "bbb", Value: "bur1"}, + }}, + }}, + }, + }}, + }, + { + desc: "several entries, slice in slice of struct", + in: map[string]string{ + "traefik/foo/0/aaa/0": "bar0", + "traefik/foo/0/aaa/1": "bar1", + "traefik/foo/1/aaa/0": "bar2", + "traefik/foo/1/aaa/1": "bar3", + }, + expected: expected{node: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "foo", Children: []*parser.Node{ + {Name: "[0]", Children: []*parser.Node{ + {Name: "aaa", Value: "bar0,bar1"}, + }}, + {Name: "[1]", Children: []*parser.Node{ + {Name: "aaa", Value: "bar2,bar3"}, + }}, + }}, + }, + }}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + out, err := DecodeToNode(mapToPairs(test.in), "traefik", test.filters...) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + + if !assert.Equal(t, test.expected.node, out) { + bytes, err := json.MarshalIndent(out, "", " ") + require.NoError(t, err) + fmt.Println(string(bytes)) + } + } + }) + } +} + +func mapToPairs(in map[string]string) []*store.KVPair { + var out []*store.KVPair + for k, v := range in { + out = append(out, &store.KVPair{Key: k, Value: []byte(v)}) + } + return out +} diff --git a/pkg/config/kv/kv_test.go b/pkg/config/kv/kv_test.go new file mode 100644 index 000000000..26f1a5e03 --- /dev/null +++ b/pkg/config/kv/kv_test.go @@ -0,0 +1,63 @@ +package kv + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecode(t *testing.T) { + pairs := mapToPairs(map[string]string{ + "traefik/fielda": "bar", + "traefik/fieldb": "1", + "traefik/fieldc": "true", + "traefik/fieldd/0": "one", + "traefik/fieldd/1": "two", + "traefik/fielde": "", + "traefik/fieldf/Test1": "A", + "traefik/fieldf/Test2": "B", + "traefik/fieldg/0/name": "A", + "traefik/fieldg/1/name": "B", + }) + + element := &sample{} + + err := Decode(pairs, element, "traefik") + require.NoError(t, err) + + expected := &sample{ + FieldA: "bar", + FieldB: 1, + FieldC: true, + FieldD: []string{"one", "two"}, + FieldE: &struct { + Name string + }{}, + FieldF: map[string]string{ + "Test1": "A", + "Test2": "B", + }, + FieldG: []sub{ + {Name: "A"}, + {Name: "B"}, + }, + } + assert.Equal(t, expected, element) +} + +type sample struct { + FieldA string + FieldB int + FieldC bool + FieldD []string + FieldE *struct { + Name string + } `label:"allowEmpty"` + FieldF map[string]string + FieldG []sub +} + +type sub struct { + Name string +} diff --git a/pkg/config/parser/element_fill.go b/pkg/config/parser/element_fill.go index 0aa0a8bdb..59b147dea 100644 --- a/pkg/config/parser/element_fill.go +++ b/pkg/config/parser/element_fill.go @@ -14,8 +14,22 @@ type initializer interface { SetDefaults() } +// FillerOpts Options for the filler. +type FillerOpts struct { + AllowSliceAsStruct bool +} + // Fill populates the fields of the element using the information in node. -func Fill(element interface{}, node *Node) error { +func Fill(element interface{}, node *Node, opts FillerOpts) error { + return filler{FillerOpts: opts}.Fill(element, node) +} + +type filler struct { + FillerOpts +} + +// Fill populates the fields of the element using the information in node. +func (f filler) Fill(element interface{}, node *Node) error { if element == nil || node == nil { return nil } @@ -29,10 +43,10 @@ func Fill(element interface{}, node *Node) error { return fmt.Errorf("struct are not supported, use pointer instead") } - return fill(root.Elem(), node) + return f.fill(root.Elem(), node) } -func fill(field reflect.Value, node *Node) error { +func (f filler) fill(field reflect.Value, node *Node) error { // related to allow-empty tag if node.Disabled { return nil @@ -70,19 +84,19 @@ func fill(field reflect.Value, node *Node) error { case reflect.Float64: return setFloat(field, node.Value, 64) case reflect.Struct: - return setStruct(field, node) + return f.setStruct(field, node) case reflect.Ptr: - return setPtr(field, node) + return f.setPtr(field, node) case reflect.Map: - return setMap(field, node) + return f.setMap(field, node) case reflect.Slice: - return setSlice(field, node) + return f.setSlice(field, node) default: return nil } } -func setPtr(field reflect.Value, node *Node) error { +func (f filler) setPtr(field reflect.Value, node *Node) error { if field.IsNil() { field.Set(reflect.New(field.Type().Elem())) @@ -94,10 +108,10 @@ func setPtr(field reflect.Value, node *Node) error { } } - return fill(field.Elem(), node) + return f.fill(field.Elem(), node) } -func setStruct(field reflect.Value, node *Node) error { +func (f filler) setStruct(field reflect.Value, node *Node) error { for _, child := range node.Children { fd := field.FieldByName(child.FieldName) @@ -106,7 +120,7 @@ func setStruct(field reflect.Value, node *Node) error { return fmt.Errorf("field not found, node: %s (%s)", child.Name, child.FieldName) } - err := fill(fd, child) + err := f.fill(fd, child) if err != nil { return err } @@ -115,10 +129,10 @@ func setStruct(field reflect.Value, node *Node) error { return nil } -func setSlice(field reflect.Value, node *Node) error { +func (f filler) setSlice(field reflect.Value, node *Node) error { if field.Type().Elem().Kind() == reflect.Struct || field.Type().Elem().Kind() == reflect.Ptr && field.Type().Elem().Elem().Kind() == reflect.Struct { - return setSliceStruct(field, node) + return f.setSliceStruct(field, node) } if len(node.Value) == 0 { @@ -211,9 +225,9 @@ func setSlice(field reflect.Value, node *Node) error { return nil } -func setSliceStruct(field reflect.Value, node *Node) error { - if node.Tag.Get(TagLabelSliceAsStruct) != "" { - return setSliceAsStruct(field, node) +func (f filler) setSliceStruct(field reflect.Value, node *Node) error { + if f.AllowSliceAsStruct && node.Tag.Get(TagLabelSliceAsStruct) != "" { + return f.setSliceAsStruct(field, node) } field.Set(reflect.MakeSlice(field.Type(), len(node.Children), len(node.Children))) @@ -221,7 +235,7 @@ func setSliceStruct(field reflect.Value, node *Node) error { for i, child := range node.Children { // use Ptr to allow "SetDefaults" value := reflect.New(reflect.PtrTo(field.Type().Elem())) - err := setPtr(value, child) + err := f.setPtr(value, child) if err != nil { return err } @@ -232,14 +246,14 @@ func setSliceStruct(field reflect.Value, node *Node) error { return nil } -func setSliceAsStruct(field reflect.Value, node *Node) error { +func (f filler) setSliceAsStruct(field reflect.Value, node *Node) error { if len(node.Children) == 0 { return fmt.Errorf("invalid slice: node %s", node.Name) } // use Ptr to allow "SetDefaults" value := reflect.New(reflect.PtrTo(field.Type().Elem())) - err := setPtr(value, node) + err := f.setPtr(value, node) if err != nil { return err } @@ -252,7 +266,7 @@ func setSliceAsStruct(field reflect.Value, node *Node) error { return nil } -func setMap(field reflect.Value, node *Node) error { +func (f filler) setMap(field reflect.Value, node *Node) error { if field.IsNil() { field.Set(reflect.MakeMap(field.Type())) } @@ -260,7 +274,7 @@ func setMap(field reflect.Value, node *Node) error { for _, child := range node.Children { ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem())) - err := fill(ptrValue, child) + err := f.fill(ptrValue, child) if err != nil { return err } diff --git a/pkg/config/parser/element_fill_test.go b/pkg/config/parser/element_fill_test.go index 664b81bd7..5843702ee 100644 --- a/pkg/config/parser/element_fill_test.go +++ b/pkg/config/parser/element_fill_test.go @@ -1390,7 +1390,7 @@ func TestFill(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - err := Fill(test.element, test.node) + err := filler{FillerOpts: FillerOpts{AllowSliceAsStruct: true}}.Fill(test.element, test.node) if test.expected.error { require.Error(t, err) } else { diff --git a/pkg/config/parser/element_nodes.go b/pkg/config/parser/element_nodes.go index af0d431fa..0f61d9248 100644 --- a/pkg/config/parser/element_nodes.go +++ b/pkg/config/parser/element_nodes.go @@ -7,13 +7,20 @@ import ( "strings" ) +// EncoderToNodeOpts Options for the encoderToNode. +type EncoderToNodeOpts struct { + OmitEmpty bool + TagName string + AllowSliceAsStruct bool +} + // EncodeToNode converts an element to a node. // element -> nodes -func EncodeToNode(element interface{}, rootName string, omitEmpty bool) (*Node, error) { +func EncodeToNode(element interface{}, rootName string, opts EncoderToNodeOpts) (*Node, error) { rValue := reflect.ValueOf(element) node := &Node{Name: rootName} - encoder := encoderToNode{omitEmpty: omitEmpty} + encoder := encoderToNode{EncoderToNodeOpts: opts} err := encoder.setNodeValue(node, rValue) if err != nil { @@ -24,7 +31,7 @@ func EncodeToNode(element interface{}, rootName string, omitEmpty bool) (*Node, } type encoderToNode struct { - omitEmpty bool + EncoderToNodeOpts } func (e encoderToNode) setNodeValue(node *Node, rValue reflect.Value) error { @@ -65,7 +72,7 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error { continue } - if field.Tag.Get(TagLabel) == "-" { + if field.Tag.Get(e.TagName) == "-" { continue } @@ -78,7 +85,7 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error { } nodeName := field.Name - if field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 { + if e.AllowSliceAsStruct && field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 { nodeName = field.Tag.Get(TagLabelSliceAsStruct) } @@ -101,7 +108,7 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error { } if field.Type.Elem().Kind() == reflect.Struct && len(child.Children) == 0 { - if field.Tag.Get(TagLabel) != TagLabelAllowEmpty { + if field.Tag.Get(e.TagName) != TagLabelAllowEmpty { continue } @@ -181,7 +188,7 @@ func (e encoderToNode) setSliceValue(node *Node, rValue reflect.Value) error { } func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool { - if e.omitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 { + if e.OmitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 { return true } @@ -189,7 +196,7 @@ func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue refl return true } - if e.omitEmpty && (field.Type.Kind() == reflect.Slice) && + if e.OmitEmpty && (field.Type.Kind() == reflect.Slice) && (fieldValue.IsNil() || fieldValue.Len() == 0) { return true } diff --git a/pkg/config/parser/element_nodes_test.go b/pkg/config/parser/element_nodes_test.go index 495b0518b..c90063f3a 100644 --- a/pkg/config/parser/element_nodes_test.go +++ b/pkg/config/parser/element_nodes_test.go @@ -723,7 +723,8 @@ func TestEncodeToNode(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - node, err := EncodeToNode(test.element, DefaultRootName, true) + etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true} + node, err := EncodeToNode(test.element, DefaultRootName, etnOpts) if test.expected.error { require.Error(t, err) diff --git a/pkg/config/parser/flat_encode.go b/pkg/config/parser/flat_encode.go index 443e7dad1..7ac8c70f9 100644 --- a/pkg/config/parser/flat_encode.go +++ b/pkg/config/parser/flat_encode.go @@ -18,6 +18,7 @@ type FlatOpts struct { Case string // "lower" or "upper", defaults to "lower". Separator string SkipRoot bool + TagName string } // Flat is a configuration item representation. @@ -69,7 +70,7 @@ func (e encoderToFlat) createFlat(field reflect.Value, name string, node *Node) var entries []Flat if node.Kind != reflect.Map && node.Description != "-" { if !(node.Kind == reflect.Ptr && len(node.Children) > 0) || - (node.Kind == reflect.Ptr && node.Tag.Get("label") == TagLabelAllowEmpty) { + (node.Kind == reflect.Ptr && node.Tag.Get(e.TagName) == TagLabelAllowEmpty) { if node.Name[0] != '[' { entries = append(entries, Flat{ Name: e.getName(name), diff --git a/pkg/config/parser/flat_encode_test.go b/pkg/config/parser/flat_encode_test.go index 69f94f42c..ba48aab0c 100644 --- a/pkg/config/parser/flat_encode_test.go +++ b/pkg/config/parser/flat_encode_test.go @@ -156,6 +156,7 @@ func TestEncodeToFlat(t *testing.T) { Case: "upper", Separator: "_", SkipRoot: false, + TagName: TagLabel, }, expected: []Flat{{ Name: "TRAEFIK_FIELD", @@ -1236,7 +1237,7 @@ func TestEncodeToFlat(t *testing.T) { var opts FlatOpts if test.opts == nil { - opts = FlatOpts{Separator: ".", SkipRoot: true} + opts = FlatOpts{Separator: ".", SkipRoot: true, TagName: TagLabel} } else { opts = *test.opts } diff --git a/pkg/config/parser/nodes_metadata.go b/pkg/config/parser/nodes_metadata.go index 3ee3d0982..3357a0bd2 100644 --- a/pkg/config/parser/nodes_metadata.go +++ b/pkg/config/parser/nodes_metadata.go @@ -7,8 +7,23 @@ import ( "strings" ) +// MetadataOpts Options for the metadata. +type MetadataOpts struct { + TagName string + AllowSliceAsStruct bool +} + // AddMetadata adds metadata such as type, inferred from element, to a node. -func AddMetadata(element interface{}, node *Node) error { +func AddMetadata(element interface{}, node *Node, opts MetadataOpts) error { + return metadata{MetadataOpts: opts}.Add(element, node) +} + +type metadata struct { + MetadataOpts +} + +// Add adds metadata such as type, inferred from element, to a node. +func (m metadata) Add(element interface{}, node *Node) error { if node == nil { return nil } @@ -24,25 +39,25 @@ func AddMetadata(element interface{}, node *Node) error { rootType := reflect.TypeOf(element) node.Kind = rootType.Kind() - return browseChildren(rootType, node) + return m.browseChildren(rootType, node) } -func browseChildren(fType reflect.Type, node *Node) error { +func (m metadata) browseChildren(fType reflect.Type, node *Node) error { for _, child := range node.Children { - if err := addMetadata(fType, child); err != nil { + if err := m.add(fType, child); err != nil { return err } } return nil } -func addMetadata(rootType reflect.Type, node *Node) error { +func (m metadata) add(rootType reflect.Type, node *Node) error { rType := rootType if rootType.Kind() == reflect.Ptr { rType = rootType.Elem() } - field, err := findTypedField(rType, node) + field, err := m.findTypedField(rType, node) if err != nil { return err } @@ -57,11 +72,11 @@ func addMetadata(rootType reflect.Type, node *Node) error { if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct || fType.Kind() == reflect.Map { - if len(node.Children) == 0 && field.Tag.Get(TagLabel) != TagLabelAllowEmpty { + if len(node.Children) == 0 && field.Tag.Get(m.TagName) != TagLabelAllowEmpty { return fmt.Errorf("%s cannot be a standalone element (type %s)", node.Name, fType) } - node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(TagLabel) == TagLabelAllowEmpty + node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(m.TagName) == TagLabelAllowEmpty } if len(node.Children) == 0 { @@ -69,7 +84,7 @@ func addMetadata(rootType reflect.Type, node *Node) error { } if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct { - return browseChildren(fType, node) + return m.browseChildren(fType, node) } if fType.Kind() == reflect.Map { @@ -80,7 +95,7 @@ func addMetadata(rootType reflect.Type, node *Node) error { if elem.Kind() == reflect.Map || elem.Kind() == reflect.Struct || (elem.Kind() == reflect.Ptr && elem.Elem().Kind() == reflect.Struct) { - if err = browseChildren(elem, child); err != nil { + if err = m.browseChildren(elem, child); err != nil { return err } } @@ -89,13 +104,13 @@ func addMetadata(rootType reflect.Type, node *Node) error { } if fType.Kind() == reflect.Slice { - if field.Tag.Get(TagLabelSliceAsStruct) != "" { - return browseChildren(fType.Elem(), node) + if m.AllowSliceAsStruct && field.Tag.Get(TagLabelSliceAsStruct) != "" { + return m.browseChildren(fType.Elem(), node) } for _, ch := range node.Children { ch.Kind = fType.Elem().Kind() - if err = browseChildren(fType.Elem(), ch); err != nil { + if err = m.browseChildren(fType.Elem(), ch); err != nil { return err } } @@ -105,19 +120,19 @@ func addMetadata(rootType reflect.Type, node *Node) error { return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind()) } -func findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) { +func (m metadata) findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) { for i := 0; i < rType.NumField(); i++ { cField := rType.Field(i) fieldName := cField.Tag.Get(TagLabelSliceAsStruct) - if len(fieldName) == 0 { + if !m.AllowSliceAsStruct || len(fieldName) == 0 { fieldName = cField.Name } if IsExported(cField) { if cField.Anonymous { if cField.Type.Kind() == reflect.Struct { - structField, err := findTypedField(cField.Type, node) + structField, err := m.findTypedField(cField.Type, node) if err != nil { continue } diff --git a/pkg/config/parser/nodes_metadata_test.go b/pkg/config/parser/nodes_metadata_test.go index 74c09cb74..c3ab1aa5f 100644 --- a/pkg/config/parser/nodes_metadata_test.go +++ b/pkg/config/parser/nodes_metadata_test.go @@ -991,7 +991,7 @@ func TestAddMetadata(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - err := AddMetadata(test.structure, test.tree) + err := metadata{MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true}}.Add(test.structure, test.tree) if test.expected.error { assert.Error(t, err) diff --git a/pkg/config/parser/parser.go b/pkg/config/parser/parser.go index 5258a5624..957a27beb 100644 --- a/pkg/config/parser/parser.go +++ b/pkg/config/parser/parser.go @@ -13,12 +13,13 @@ func Decode(labels map[string]string, element interface{}, rootName string, filt return err } - err = AddMetadata(element, node) + metaOpts := MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true} + err = AddMetadata(element, node, metaOpts) if err != nil { return err } - err = Fill(element, node) + err = Fill(element, node, FillerOpts{AllowSliceAsStruct: true}) if err != nil { return err } @@ -29,7 +30,8 @@ func Decode(labels map[string]string, element interface{}, rootName string, filt // Encode converts an element to labels. // element -> node (value) -> label (node) func Encode(element interface{}, rootName string) (map[string]string, error) { - node, err := EncodeToNode(element, rootName, true) + etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true} + node, err := EncodeToNode(element, rootName, etnOpts) if err != nil { return nil, err } diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index a10648a0d..1aa25f359 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -14,6 +14,10 @@ import ( "github.com/containous/traefik/v2/pkg/provider/file" "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd" "github.com/containous/traefik/v2/pkg/provider/kubernetes/ingress" + "github.com/containous/traefik/v2/pkg/provider/kv/consul" + "github.com/containous/traefik/v2/pkg/provider/kv/etcd" + "github.com/containous/traefik/v2/pkg/provider/kv/redis" + "github.com/containous/traefik/v2/pkg/provider/kv/zk" "github.com/containous/traefik/v2/pkg/provider/marathon" "github.com/containous/traefik/v2/pkg/provider/rancher" "github.com/containous/traefik/v2/pkg/provider/rest" @@ -156,15 +160,21 @@ func (t *Tracing) SetDefaults() { // Providers contains providers configuration type Providers struct { - ProvidersThrottleDuration types.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." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"` - Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty"` - File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"` - Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty"` - KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty"` - KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty"` - Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty"` - Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty"` - ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"` + ProvidersThrottleDuration types.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." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"` + + Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty"` + File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"` + Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty"` + KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty"` + KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty"` + Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty"` + Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty"` + ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"` + + Consul *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty"` + Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty"` + ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" export:"true" label:"allowEmpty"` + Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true" label:"allowEmpty"` } // SetEffectiveConfiguration adds missing configuration parameters derived from existing ones. diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 7bcdee70c..526d0c620 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -53,6 +53,22 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { p.quietAddProvider(conf.ConsulCatalog) } + if conf.Consul != nil { + p.quietAddProvider(conf.Consul) + } + + if conf.Etcd != nil { + p.quietAddProvider(conf.Etcd) + } + + if conf.ZooKeeper != nil { + p.quietAddProvider(conf.ZooKeeper) + } + + if conf.Redis != nil { + p.quietAddProvider(conf.Redis) + } + return p } diff --git a/pkg/provider/kv/consul/consul.go b/pkg/provider/kv/consul/consul.go new file mode 100644 index 000000000..8cefad995 --- /dev/null +++ b/pkg/provider/kv/consul/consul.go @@ -0,0 +1,25 @@ +package consul + +import ( + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/provider" + "github.com/containous/traefik/v2/pkg/provider/kv" +) + +var _ provider.Provider = (*Provider)(nil) + +// Provider holds configurations of the provider. +type Provider struct { + kv.Provider +} + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.Provider.SetDefaults() + p.Endpoints = []string{"127.0.0.1:8500"} +} + +// Init the provider +func (p *Provider) Init() error { + return p.Provider.Init(store.CONSUL, "consul") +} diff --git a/pkg/provider/kv/etcd/etcd.go b/pkg/provider/kv/etcd/etcd.go new file mode 100644 index 000000000..5d104c742 --- /dev/null +++ b/pkg/provider/kv/etcd/etcd.go @@ -0,0 +1,25 @@ +package etcd + +import ( + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/provider" + "github.com/containous/traefik/v2/pkg/provider/kv" +) + +var _ provider.Provider = (*Provider)(nil) + +// Provider holds configurations of the provider. +type Provider struct { + kv.Provider +} + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.Provider.SetDefaults() + p.Endpoints = []string{"127.0.0.1:2379"} +} + +// Init the provider +func (p *Provider) Init() error { + return p.Provider.Init(store.ETCDV3, "etcd") +} diff --git a/pkg/provider/kv/kv.go b/pkg/provider/kv/kv.go new file mode 100644 index 000000000..e10f5cca7 --- /dev/null +++ b/pkg/provider/kv/kv.go @@ -0,0 +1,191 @@ +package kv + +import ( + "context" + "errors" + "fmt" + "path" + "time" + + "github.com/abronan/valkeyrie" + "github.com/abronan/valkeyrie/store" + "github.com/abronan/valkeyrie/store/consul" + etcdv3 "github.com/abronan/valkeyrie/store/etcd/v3" + "github.com/abronan/valkeyrie/store/redis" + "github.com/abronan/valkeyrie/store/zookeeper" + "github.com/cenkalti/backoff/v3" + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/kv" + "github.com/containous/traefik/v2/pkg/job" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/safe" + "github.com/containous/traefik/v2/pkg/types" +) + +// Provider holds configurations of the provider. +type Provider struct { + RootKey string `description:"Root key used for KV store" export:"true" json:"rootKey,omitempty" toml:"rootKey,omitempty" yaml:"rootKey,omitempty"` + + Endpoints []string `description:"KV store endpoints" json:"endpoints,omitempty" toml:"endpoints,omitempty" yaml:"endpoints,omitempty"` + Username string `description:"KV Username" json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty"` + Password string `description:"KV Password" json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty"` + TLS *types.ClientTLS `description:"Enable TLS support" export:"true" json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty"` + + storeType store.Backend + kvClient store.Store + name string +} + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.RootKey = "traefik" +} + +// Init the provider +func (p *Provider) Init(storeType store.Backend, name string) error { + ctx := log.With(context.Background(), log.Str(log.ProviderName, string(storeType))) + + p.storeType = storeType + p.name = name + + kvClient, err := p.createKVClient(ctx) + if err != nil { + return fmt.Errorf("failed to Connect to KV store: %w", err) + } + + p.kvClient = kvClient + + return nil +} + +// Provide allows the docker provider to provide configurations to traefik using the given configuration channel. +func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { + ctx := log.With(context.Background(), log.Str(log.ProviderName, string(p.storeType))) + + logger := log.FromContext(ctx) + + operation := func() error { + if _, err := p.kvClient.Exists(path.Join(p.RootKey, "qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"), nil); err != nil { + return fmt.Errorf("KV store connection error: %w", err) + } + return nil + } + + notify := func(err error, time time.Duration) { + logger.Errorf("KV connection error: %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) + if err != nil { + return fmt.Errorf("cannot connect to KV server: %w", err) + } + + configuration, err := p.buildConfiguration() + if err != nil { + logger.Errorf("Cannot build the configuration: %v", err) + } else { + configurationChan <- dynamic.Message{ + ProviderName: p.name, + Configuration: configuration, + } + } + + pool.Go(func(stop chan bool) { + err := p.watchKv(ctx, configurationChan, p.RootKey, stop) + if err != nil { + logger.Errorf("Cannot watch KV store: %v", err) + } + }) + + return nil +} + +func (p *Provider) watchKv(ctx context.Context, configurationChan chan<- dynamic.Message, prefix string, stop chan bool) error { + operation := func() error { + events, err := p.kvClient.WatchTree(p.RootKey, make(chan struct{}), nil) + if err != nil { + return fmt.Errorf("failed to watch KV: %w", err) + } + + for { + select { + case <-stop: + return nil + case _, ok := <-events: + if !ok { + return errors.New("the WatchTree channel is closed") + } + + configuration, errC := p.buildConfiguration() + if errC != nil { + return errC + } + + if configuration != nil { + configurationChan <- dynamic.Message{ + ProviderName: string(p.storeType), + Configuration: configuration, + } + } + } + } + } + + notify := func(err error, time time.Duration) { + log.FromContext(ctx).Errorf("KV connection error: %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) + if err != nil { + return fmt.Errorf("cannot connect to KV server: %w", err) + } + return nil +} + +func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) { + pairs, err := p.kvClient.List(p.RootKey, nil) + if err != nil { + return nil, err + } + + cfg := &dynamic.Configuration{} + err = kv.Decode(pairs, cfg, p.RootKey) + if err != nil { + return nil, err + } + + return cfg, nil +} + +func (p *Provider) createKVClient(ctx context.Context) (store.Store, error) { + storeConfig := &store.Config{ + ConnectionTimeout: 3 * time.Second, + Bucket: "traefik", + Username: p.Username, + Password: p.Password, + } + + if p.TLS != nil { + var err error + storeConfig.TLS, err = p.TLS.CreateTLSConfig(ctx) + if err != nil { + return nil, err + } + } + + switch p.storeType { + case store.CONSUL: + consul.Register() + case store.ETCDV3: + etcdv3.Register() + case store.ZK: + zookeeper.Register() + case store.REDIS: + redis.Register() + } + + kvStore, err := valkeyrie.NewStore(p.storeType, p.Endpoints, storeConfig) + if err != nil { + return nil, err + } + + return &storeWrapper{Store: kvStore}, nil +} diff --git a/pkg/provider/kv/kv_mock_test.go b/pkg/provider/kv/kv_mock_test.go new file mode 100644 index 000000000..0d63d8825 --- /dev/null +++ b/pkg/provider/kv/kv_mock_test.go @@ -0,0 +1,122 @@ +package kv + +import ( + "errors" + "strings" + + "github.com/abronan/valkeyrie/store" +) + +func newProviderMock(kvPairs []*store.KVPair) *Provider { + return &Provider{ + RootKey: "traefik", + kvClient: newKvClientMock(kvPairs, nil), + } +} + +// Override Get/List to return a error +type KvError struct { + Get error + List error +} + +// Extremely limited mock store so we can test initialization +type Mock struct { + Error KvError + KVPairs []*store.KVPair + WatchTreeMethod func() <-chan []*store.KVPair +} + +func newKvClientMock(kvPairs []*store.KVPair, err error) *Mock { + mock := &Mock{ + KVPairs: kvPairs, + } + + if err != nil { + mock.Error = KvError{ + Get: err, + List: err, + } + } + return mock +} + +func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error { + return errors.New("method Put not supported") +} + +func (s *Mock) Get(key string, options *store.ReadOptions) (*store.KVPair, error) { + if err := s.Error.Get; err != nil { + return nil, err + } + for _, kvPair := range s.KVPairs { + if kvPair.Key == key { + return kvPair, nil + } + } + return nil, store.ErrKeyNotFound +} + +func (s *Mock) Delete(key string) error { + return errors.New("method Delete not supported") +} + +// Exists mock +func (s *Mock) Exists(key string, options *store.ReadOptions) (bool, error) { + if err := s.Error.Get; err != nil { + return false, err + } + for _, kvPair := range s.KVPairs { + if strings.HasPrefix(kvPair.Key, key) { + return true, nil + } + } + return false, store.ErrKeyNotFound +} + +// Watch mock +func (s *Mock) Watch(key string, stopCh <-chan struct{}, options *store.ReadOptions) (<-chan *store.KVPair, error) { + return nil, errors.New("method Watch not supported") +} + +// WatchTree mock +func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}, options *store.ReadOptions) (<-chan []*store.KVPair, error) { + return s.WatchTreeMethod(), nil +} + +// NewLock mock +func (s *Mock) NewLock(key string, options *store.LockOptions) (store.Locker, error) { + return nil, errors.New("method NewLock not supported") +} + +// List mock +func (s *Mock) List(prefix string, options *store.ReadOptions) ([]*store.KVPair, error) { + if err := s.Error.List; err != nil { + return nil, err + } + var kv []*store.KVPair + for _, kvPair := range s.KVPairs { + if strings.HasPrefix(kvPair.Key, prefix) { // FIXME && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") { + kv = append(kv, kvPair) + } + } + return kv, nil +} + +// DeleteTree mock +func (s *Mock) DeleteTree(prefix string) error { + return errors.New("method DeleteTree not supported") +} + +// AtomicPut mock +func (s *Mock) AtomicPut(key string, value []byte, previous *store.KVPair, opts *store.WriteOptions) (bool, *store.KVPair, error) { + return false, nil, errors.New("method AtomicPut not supported") +} + +// AtomicDelete mock +func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) { + return false, errors.New("method AtomicDelete not supported") +} + +// Close mock +func (s *Mock) Close() {} diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go new file mode 100644 index 000000000..d30cf8f1d --- /dev/null +++ b/pkg/provider/kv/kv_test.go @@ -0,0 +1,892 @@ +package kv + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/tls" + "github.com/containous/traefik/v2/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_buildConfiguration(t *testing.T) { + provider := newProviderMock(mapToPairs(map[string]string{ + "traefik/http/routers/Router0/entryPoints/0": "foobar", + "traefik/http/routers/Router0/entryPoints/1": "foobar", + "traefik/http/routers/Router0/middlewares/0": "foobar", + "traefik/http/routers/Router0/middlewares/1": "foobar", + "traefik/http/routers/Router0/service": "foobar", + "traefik/http/routers/Router0/rule": "foobar", + "traefik/http/routers/Router0/priority": "42", + "traefik/http/routers/Router0/tls": "", + "traefik/http/routers/Router1/rule": "foobar", + "traefik/http/routers/Router1/priority": "42", + "traefik/http/routers/Router1/tls/domains/0/main": "foobar", + "traefik/http/routers/Router1/tls/domains/0/sans/0": "foobar", + "traefik/http/routers/Router1/tls/domains/0/sans/1": "foobar", + "traefik/http/routers/Router1/tls/domains/1/main": "foobar", + "traefik/http/routers/Router1/tls/domains/1/sans/0": "foobar", + "traefik/http/routers/Router1/tls/domains/1/sans/1": "foobar", + "traefik/http/routers/Router1/tls/options": "foobar", + "traefik/http/routers/Router1/tls/certResolver": "foobar", + "traefik/http/routers/Router1/entryPoints/0": "foobar", + "traefik/http/routers/Router1/entryPoints/1": "foobar", + "traefik/http/routers/Router1/middlewares/0": "foobar", + "traefik/http/routers/Router1/middlewares/1": "foobar", + "traefik/http/routers/Router1/service": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/path": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/port": "42", + "traefik/http/services/Service01/loadBalancer/healthCheck/interval": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/timeout": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/hostname": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/headers/name1": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/scheme": "foobar", + "traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval": "foobar", + "traefik/http/services/Service01/loadBalancer/passHostHeader": "true", + "traefik/http/services/Service01/loadBalancer/sticky/cookie/name": "foobar", + "traefik/http/services/Service01/loadBalancer/sticky/cookie/secure": "true", + "traefik/http/services/Service01/loadBalancer/sticky/cookie/httpOnly": "true", + "traefik/http/services/Service01/loadBalancer/servers/0/url": "foobar", + "traefik/http/services/Service01/loadBalancer/servers/1/url": "foobar", + "traefik/http/services/Service02/mirroring/service": "foobar", + "traefik/http/services/Service02/mirroring/mirrors/0/name": "foobar", + "traefik/http/services/Service02/mirroring/mirrors/0/percent": "42", + "traefik/http/services/Service02/mirroring/mirrors/1/name": "foobar", + "traefik/http/services/Service02/mirroring/mirrors/1/percent": "42", + "traefik/http/services/Service03/weighted/sticky/cookie/name": "foobar", + "traefik/http/services/Service03/weighted/sticky/cookie/secure": "true", + "traefik/http/services/Service03/weighted/sticky/cookie/httpOnly": "true", + "traefik/http/services/Service03/weighted/services/0/name": "foobar", + "traefik/http/services/Service03/weighted/services/0/weight": "42", + "traefik/http/services/Service03/weighted/services/1/name": "foobar", + "traefik/http/services/Service03/weighted/services/1/weight": "42", + "traefik/http/middlewares/Middleware08/forwardAuth/authResponseHeaders/0": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/authResponseHeaders/1": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/tls/key": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/tls/insecureSkipVerify": "true", + "traefik/http/middlewares/Middleware08/forwardAuth/tls/ca": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/tls/caOptional": "true", + "traefik/http/middlewares/Middleware08/forwardAuth/tls/cert": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/address": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader": "true", + "traefik/http/middlewares/Middleware15/redirectScheme/scheme": "foobar", + "traefik/http/middlewares/Middleware15/redirectScheme/port": "foobar", + "traefik/http/middlewares/Middleware15/redirectScheme/permanent": "true", + "traefik/http/middlewares/Middleware17/replacePathRegex/regex": "foobar", + "traefik/http/middlewares/Middleware17/replacePathRegex/replacement": "foobar", + "traefik/http/middlewares/Middleware14/redirectRegex/regex": "foobar", + "traefik/http/middlewares/Middleware14/redirectRegex/replacement": "foobar", + "traefik/http/middlewares/Middleware14/redirectRegex/permanent": "true", + "traefik/http/middlewares/Middleware16/replacePath/path": "foobar", + "traefik/http/middlewares/Middleware06/digestAuth/removeHeader": "true", + "traefik/http/middlewares/Middleware06/digestAuth/realm": "foobar", + "traefik/http/middlewares/Middleware06/digestAuth/headerField": "foobar", + "traefik/http/middlewares/Middleware06/digestAuth/users/0": "foobar", + "traefik/http/middlewares/Middleware06/digestAuth/users/1": "foobar", + "traefik/http/middlewares/Middleware06/digestAuth/usersFile": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/0": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/1": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowOrigin": "foobar", + "traefik/http/middlewares/Middleware09/headers/contentTypeNosniff": "true", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials": "true", + "traefik/http/middlewares/Middleware09/headers/featurePolicy": "foobar", + "traefik/http/middlewares/Middleware09/headers/forceSTSHeader": "true", + "traefik/http/middlewares/Middleware09/headers/sslRedirect": "true", + "traefik/http/middlewares/Middleware09/headers/sslHost": "foobar", + "traefik/http/middlewares/Middleware09/headers/sslForceHost": "true", + "traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name1": "foobar", + "traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name0": "foobar", + "traefik/http/middlewares/Middleware09/headers/allowedHosts/0": "foobar", + "traefik/http/middlewares/Middleware09/headers/allowedHosts/1": "foobar", + "traefik/http/middlewares/Middleware09/headers/stsPreload": "true", + "traefik/http/middlewares/Middleware09/headers/frameDeny": "true", + "traefik/http/middlewares/Middleware09/headers/isDevelopment": "true", + "traefik/http/middlewares/Middleware09/headers/customResponseHeaders/name1": "foobar", + "traefik/http/middlewares/Middleware09/headers/customResponseHeaders/name0": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowMethods/0": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowMethods/1": "foobar", + "traefik/http/middlewares/Middleware09/headers/stsSeconds": "42", + "traefik/http/middlewares/Middleware09/headers/stsIncludeSubdomains": "true", + "traefik/http/middlewares/Middleware09/headers/customFrameOptionsValue": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlMaxAge": "42", + "traefik/http/middlewares/Middleware09/headers/addVaryHeader": "true", + "traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/0": "foobar", + "traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/1": "foobar", + "traefik/http/middlewares/Middleware09/headers/sslTemporaryRedirect": "true", + "traefik/http/middlewares/Middleware09/headers/customBrowserXSSValue": "foobar", + "traefik/http/middlewares/Middleware09/headers/referrerPolicy": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/0": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/1": "foobar", + "traefik/http/middlewares/Middleware09/headers/contentSecurityPolicy": "foobar", + "traefik/http/middlewares/Middleware09/headers/publicKey": "foobar", + "traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name0": "foobar", + "traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name1": "foobar", + "traefik/http/middlewares/Middleware09/headers/browserXssFilter": "true", + "traefik/http/middlewares/Middleware10/ipWhiteList/sourceRange/0": "foobar", + "traefik/http/middlewares/Middleware10/ipWhiteList/sourceRange/1": "foobar", + "traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/excludedIPs/0": "foobar", + "traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/excludedIPs/1": "foobar", + "traefik/http/middlewares/Middleware10/ipWhiteList/ipStrategy/depth": "42", + "traefik/http/middlewares/Middleware11/inFlightReq/amount": "42", + "traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/requestHost": "true", + "traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/depth": "42", + "traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/0": "foobar", + "traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1": "foobar", + "traefik/http/middlewares/Middleware11/inFlightReq/sourceCriterion/requestHeaderName": "foobar", + "traefik/http/middlewares/Middleware12/passTLSClientCert/pem": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/notAfter": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/notBefore": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/sans": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/country": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/province": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/locality": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/organization": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/commonName": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/serialNumber": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/domainComponent": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/country": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/province": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/locality": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/organization": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/commonName": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/serialNumber": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/issuer/domainComponent": "true", + "traefik/http/middlewares/Middleware00/addPrefix/prefix": "foobar", + "traefik/http/middlewares/Middleware03/chain/middlewares/0": "foobar", + "traefik/http/middlewares/Middleware03/chain/middlewares/1": "foobar", + "traefik/http/middlewares/Middleware04/circuitBreaker/expression": "foobar", + "traefik/http/middlewares/Middleware07/errors/status/0": "foobar", + "traefik/http/middlewares/Middleware07/errors/status/1": "foobar", + "traefik/http/middlewares/Middleware07/errors/service": "foobar", + "traefik/http/middlewares/Middleware07/errors/query": "foobar", + "traefik/http/middlewares/Middleware13/rateLimit/average": "42", + "traefik/http/middlewares/Middleware13/rateLimit/burst": "42", + "traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/requestHeaderName": "foobar", + "traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/requestHost": "true", + "traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/depth": "42", + "traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0": "foobar", + "traefik/http/middlewares/Middleware13/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1": "foobar", + "traefik/http/middlewares/Middleware20/stripPrefixRegex/regex/0": "foobar", + "traefik/http/middlewares/Middleware20/stripPrefixRegex/regex/1": "foobar", + "traefik/http/middlewares/Middleware01/basicAuth/users/0": "foobar", + "traefik/http/middlewares/Middleware01/basicAuth/users/1": "foobar", + "traefik/http/middlewares/Middleware01/basicAuth/usersFile": "foobar", + "traefik/http/middlewares/Middleware01/basicAuth/realm": "foobar", + "traefik/http/middlewares/Middleware01/basicAuth/removeHeader": "true", + "traefik/http/middlewares/Middleware01/basicAuth/headerField": "foobar", + "traefik/http/middlewares/Middleware02/buffering/maxResponseBodyBytes": "42", + "traefik/http/middlewares/Middleware02/buffering/memResponseBodyBytes": "42", + "traefik/http/middlewares/Middleware02/buffering/retryExpression": "foobar", + "traefik/http/middlewares/Middleware02/buffering/maxRequestBodyBytes": "42", + "traefik/http/middlewares/Middleware02/buffering/memRequestBodyBytes": "42", + "traefik/http/middlewares/Middleware05/compress": "", + "traefik/http/middlewares/Middleware18/retry/attempts": "42", + "traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0": "foobar", + "traefik/http/middlewares/Middleware19/stripPrefix/prefixes/1": "foobar", + "traefik/http/middlewares/Middleware19/stripPrefix/forceSlash": "true", + "traefik/tcp/routers/TCPRouter0/entryPoints/0": "foobar", + "traefik/tcp/routers/TCPRouter0/entryPoints/1": "foobar", + "traefik/tcp/routers/TCPRouter0/service": "foobar", + "traefik/tcp/routers/TCPRouter0/rule": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/options": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/certResolver": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/domains/0/main": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/domains/0/sans/0": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/domains/0/sans/1": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/domains/1/main": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/domains/1/sans/0": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/domains/1/sans/1": "foobar", + "traefik/tcp/routers/TCPRouter0/tls/passthrough": "true", + "traefik/tcp/routers/TCPRouter1/entryPoints/0": "foobar", + "traefik/tcp/routers/TCPRouter1/entryPoints/1": "foobar", + "traefik/tcp/routers/TCPRouter1/service": "foobar", + "traefik/tcp/routers/TCPRouter1/rule": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/domains/0/main": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/domains/0/sans/0": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/domains/0/sans/1": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/domains/1/main": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/domains/1/sans/0": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/domains/1/sans/1": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/passthrough": "true", + "traefik/tcp/routers/TCPRouter1/tls/options": "foobar", + "traefik/tcp/routers/TCPRouter1/tls/certResolver": "foobar", + "traefik/tcp/services/TCPService01/loadBalancer/terminationDelay": "42", + "traefik/tcp/services/TCPService01/loadBalancer/servers/0/address": "foobar", + "traefik/tcp/services/TCPService01/loadBalancer/servers/1/address": "foobar", + "traefik/tcp/services/TCPService02/weighted/services/0/name": "foobar", + "traefik/tcp/services/TCPService02/weighted/services/0/weight": "42", + "traefik/tcp/services/TCPService02/weighted/services/1/name": "foobar", + "traefik/tcp/services/TCPService02/weighted/services/1/weight": "43", + "traefik/tls/options/Options0/minVersion": "foobar", + "traefik/tls/options/Options0/maxVersion": "foobar", + "traefik/tls/options/Options0/cipherSuites/0": "foobar", + "traefik/tls/options/Options0/cipherSuites/1": "foobar", + "traefik/tls/options/Options0/sniStrict": "true", + "traefik/tls/options/Options0/curvePreferences/0": "foobar", + "traefik/tls/options/Options0/curvePreferences/1": "foobar", + "traefik/tls/options/Options0/clientAuth/caFiles/0": "foobar", + "traefik/tls/options/Options0/clientAuth/caFiles/1": "foobar", + "traefik/tls/options/Options0/clientAuth/clientAuthType": "foobar", + "traefik/tls/options/Options1/sniStrict": "true", + "traefik/tls/options/Options1/curvePreferences/0": "foobar", + "traefik/tls/options/Options1/curvePreferences/1": "foobar", + "traefik/tls/options/Options1/clientAuth/caFiles/0": "foobar", + "traefik/tls/options/Options1/clientAuth/caFiles/1": "foobar", + "traefik/tls/options/Options1/clientAuth/clientAuthType": "foobar", + "traefik/tls/options/Options1/minVersion": "foobar", + "traefik/tls/options/Options1/maxVersion": "foobar", + "traefik/tls/options/Options1/cipherSuites/0": "foobar", + "traefik/tls/options/Options1/cipherSuites/1": "foobar", + "traefik/tls/stores/Store0/defaultCertificate/certFile": "foobar", + "traefik/tls/stores/Store0/defaultCertificate/keyFile": "foobar", + "traefik/tls/stores/Store1/defaultCertificate/certFile": "foobar", + "traefik/tls/stores/Store1/defaultCertificate/keyFile": "foobar", + "traefik/tls/certificates/0/certFile": "foobar", + "traefik/tls/certificates/0/keyFile": "foobar", + "traefik/tls/certificates/0/stores/0": "foobar", + "traefik/tls/certificates/0/stores/1": "foobar", + "traefik/tls/certificates/1/certFile": "foobar", + "traefik/tls/certificates/1/keyFile": "foobar", + "traefik/tls/certificates/1/stores/0": "foobar", + "traefik/tls/certificates/1/stores/1": "foobar", + })) + + cfg, err := provider.buildConfiguration() + require.NoError(t, err) + + expected := &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Router1": { + EntryPoints: []string{ + "foobar", + "foobar", + }, + Middlewares: []string{ + "foobar", + "foobar", + }, + Service: "foobar", + Rule: "foobar", + Priority: 42, + TLS: &dynamic.RouterTLSConfig{ + Options: "foobar", + CertResolver: "foobar", + Domains: []types.Domain{ + { + Main: "foobar", + SANs: []string{ + "foobar", + "foobar", + }, + }, + { + Main: "foobar", + SANs: []string{ + "foobar", + "foobar", + }, + }, + }, + }, + }, + "Router0": { + EntryPoints: []string{ + "foobar", + "foobar", + }, + Middlewares: []string{ + "foobar", + "foobar", + }, + Service: "foobar", + Rule: "foobar", + Priority: 42, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "Middleware10": { + IPWhiteList: &dynamic.IPWhiteList{ + SourceRange: []string{ + "foobar", + "foobar", + }, + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{ + "foobar", + "foobar", + }, + }, + }, + }, + "Middleware13": { + RateLimit: &dynamic.RateLimit{ + Average: 42, + Burst: 42, + SourceCriterion: &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{ + "foobar", + "foobar", + }, + }, + RequestHeaderName: "foobar", + RequestHost: true, + }, + }, + }, + "Middleware19": { + StripPrefix: &dynamic.StripPrefix{ + Prefixes: []string{ + "foobar", + "foobar", + }, + ForceSlash: true, + }, + }, + "Middleware00": { + AddPrefix: &dynamic.AddPrefix{ + Prefix: "foobar", + }, + }, + "Middleware02": { + Buffering: &dynamic.Buffering{ + MaxRequestBodyBytes: 42, + MemRequestBodyBytes: 42, + MaxResponseBodyBytes: 42, + MemResponseBodyBytes: 42, + RetryExpression: "foobar", + }, + }, + "Middleware04": { + CircuitBreaker: &dynamic.CircuitBreaker{ + Expression: "foobar", + }, + }, + "Middleware05": { + Compress: &dynamic.Compress{}, + }, + "Middleware08": { + ForwardAuth: &dynamic.ForwardAuth{ + Address: "foobar", + TLS: &dynamic.ClientTLS{ + CA: "foobar", + CAOptional: true, + Cert: "foobar", + Key: "foobar", + InsecureSkipVerify: true, + }, + TrustForwardHeader: true, + AuthResponseHeaders: []string{ + "foobar", + "foobar", + }, + }, + }, + "Middleware06": { + DigestAuth: &dynamic.DigestAuth{ + Users: dynamic.Users{ + "foobar", + "foobar", + }, + UsersFile: "foobar", + RemoveHeader: true, + Realm: "foobar", + HeaderField: "foobar", + }, + }, + "Middleware18": { + Retry: &dynamic.Retry{ + Attempts: 42, + }, + }, + "Middleware16": { + ReplacePath: &dynamic.ReplacePath{ + Path: "foobar", + }, + }, + "Middleware20": { + StripPrefixRegex: &dynamic.StripPrefixRegex{ + Regex: []string{ + "foobar", + "foobar", + }, + }, + }, + "Middleware03": { + Chain: &dynamic.Chain{ + Middlewares: []string{ + "foobar", + "foobar", + }, + }, + }, + "Middleware11": { + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + SourceCriterion: &dynamic.SourceCriterion{ + IPStrategy: &dynamic.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{ + "foobar", + "foobar", + }, + }, + RequestHeaderName: "foobar", + RequestHost: true, + }, + }, + }, + "Middleware12": { + PassTLSClientCert: &dynamic.PassTLSClientCert{ + PEM: true, + Info: &dynamic.TLSClientCertificateInfo{ + NotAfter: true, + NotBefore: true, + Sans: true, + Subject: &dynamic.TLSCLientCertificateDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, + }, + Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, + }, + }, + }, + }, + "Middleware14": { + RedirectRegex: &dynamic.RedirectRegex{ + Regex: "foobar", + Replacement: "foobar", + Permanent: true, + }, + }, + "Middleware15": { + RedirectScheme: &dynamic.RedirectScheme{ + Scheme: "foobar", + Port: "foobar", + Permanent: true, + }, + }, + "Middleware01": { + BasicAuth: &dynamic.BasicAuth{ + Users: dynamic.Users{ + "foobar", + "foobar", + }, + UsersFile: "foobar", + Realm: "foobar", + RemoveHeader: true, + HeaderField: "foobar", + }, + }, + "Middleware07": { + Errors: &dynamic.ErrorPage{ + Status: []string{ + "foobar", + "foobar", + }, + Service: "foobar", + Query: "foobar", + }, + }, + "Middleware09": { + Headers: &dynamic.Headers{ + CustomRequestHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + CustomResponseHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + AccessControlAllowCredentials: true, + AccessControlAllowHeaders: []string{ + "foobar", + "foobar", + }, + AccessControlAllowMethods: []string{ + "foobar", + "foobar", + }, + AccessControlAllowOrigin: "foobar", + AccessControlExposeHeaders: []string{ + "foobar", + "foobar", + }, + AccessControlMaxAge: 42, + AddVaryHeader: true, + AllowedHosts: []string{ + "foobar", + "foobar", + }, + HostsProxyHeaders: []string{ + "foobar", + "foobar", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foobar", + SSLProxyHeaders: map[string]string{ + "name1": "foobar", + "name0": "foobar", + }, + SSLForceHost: true, + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: "foobar", + IsDevelopment: true, + }, + }, + "Middleware17": { + ReplacePathRegex: &dynamic.ReplacePathRegex{ + Regex: "foobar", + Replacement: "foobar", + }, + }, + }, + Services: map[string]*dynamic.Service{ + "Service01": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "foobar", + Secure: true, + HTTPOnly: true, + }, + }, + Servers: []dynamic.Server{ + { + URL: "foobar", + Scheme: "http", + }, + { + URL: "foobar", + Scheme: "http", + }, + }, + HealthCheck: &dynamic.HealthCheck{ + Scheme: "foobar", + Path: "foobar", + Port: 42, + Interval: "foobar", + Timeout: "foobar", + Hostname: "foobar", + Headers: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + }, + PassHostHeader: func(v bool) *bool { return &v }(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: "foobar", + }, + }, + }, + "Service02": { + Mirroring: &dynamic.Mirroring{ + Service: "foobar", + Mirrors: []dynamic.MirrorService{ + { + Name: "foobar", + Percent: 42, + }, + { + Name: "foobar", + Percent: 42, + }, + }, + }, + }, + "Service03": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "foobar", + Weight: func(v int) *int { return &v }(42), + }, + { + Name: "foobar", + Weight: func(v int) *int { return &v }(42), + }, + }, + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "foobar", + Secure: true, + HTTPOnly: true, + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "TCPRouter0": { + EntryPoints: []string{ + "foobar", + "foobar", + }, + Service: "foobar", + Rule: "foobar", + TLS: &dynamic.RouterTCPTLSConfig{ + Passthrough: true, + Options: "foobar", + CertResolver: "foobar", + Domains: []types.Domain{ + { + Main: "foobar", + SANs: []string{ + "foobar", + "foobar", + }, + }, + { + Main: "foobar", + SANs: []string{ + "foobar", + "foobar", + }, + }, + }, + }, + }, + "TCPRouter1": { + EntryPoints: []string{ + "foobar", + "foobar", + }, + Service: "foobar", + Rule: "foobar", + TLS: &dynamic.RouterTCPTLSConfig{ + Passthrough: true, + Options: "foobar", + CertResolver: "foobar", + Domains: []types.Domain{ + { + Main: "foobar", + SANs: []string{ + "foobar", + "foobar", + }, + }, + { + Main: "foobar", + SANs: []string{ + "foobar", + "foobar", + }, + }, + }, + }, + }, + }, + Services: map[string]*dynamic.TCPService{ + "TCPService01": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + TerminationDelay: func(v int) *int { return &v }(42), + Servers: []dynamic.TCPServer{ + {Address: "foobar"}, + {Address: "foobar"}, + }, + }, + }, + "TCPService02": { + Weighted: &dynamic.TCPWeightedRoundRobin{ + Services: []dynamic.TCPWRRService{ + { + Name: "foobar", + Weight: func(v int) *int { return &v }(42), + }, + { + Name: "foobar", + Weight: func(v int) *int { return &v }(43), + }, + }, + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: tls.FileOrContent("foobar"), + KeyFile: tls.FileOrContent("foobar"), + }, + Stores: []string{ + "foobar", + "foobar", + }, + }, + { + Certificate: tls.Certificate{ + CertFile: tls.FileOrContent("foobar"), + KeyFile: tls.FileOrContent("foobar"), + }, + Stores: []string{ + "foobar", + "foobar", + }, + }, + }, + Options: map[string]tls.Options{ + "Options0": { + MinVersion: "foobar", + MaxVersion: "foobar", + CipherSuites: []string{ + "foobar", + "foobar", + }, + CurvePreferences: []string{ + "foobar", + "foobar", + }, + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ + tls.FileOrContent("foobar"), + tls.FileOrContent("foobar"), + }, + ClientAuthType: "foobar", + }, + SniStrict: true, + }, + "Options1": { + MinVersion: "foobar", + MaxVersion: "foobar", + CipherSuites: []string{ + "foobar", + "foobar", + }, + CurvePreferences: []string{ + "foobar", + "foobar", + }, + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ + tls.FileOrContent("foobar"), + tls.FileOrContent("foobar"), + }, + ClientAuthType: "foobar", + }, + SniStrict: true, + }, + }, + Stores: map[string]tls.Store{ + "Store0": { + DefaultCertificate: &tls.Certificate{ + CertFile: tls.FileOrContent("foobar"), + KeyFile: tls.FileOrContent("foobar"), + }, + }, + "Store1": { + DefaultCertificate: &tls.Certificate{ + CertFile: tls.FileOrContent("foobar"), + KeyFile: tls.FileOrContent("foobar"), + }, + }, + }, + }, + } + + assert.Equal(t, expected, cfg) +} + +func Test_buildConfiguration_KV_error(t *testing.T) { + provider := &Provider{ + RootKey: "traefik", + kvClient: &Mock{ + Error: KvError{ + List: errors.New("OOPS"), + }, + KVPairs: mapToPairs(map[string]string{ + "traefik/foo": "bar", + }), + }, + } + + cfg, err := provider.buildConfiguration() + require.Error(t, err) + assert.Nil(t, cfg) +} + +func TestKvWatchTree(t *testing.T) { + returnedChans := make(chan chan []*store.KVPair) + provider := Provider{ + kvClient: &Mock{ + WatchTreeMethod: func() <-chan []*store.KVPair { + c := make(chan []*store.KVPair, 10) + returnedChans <- c + return c + }, + }, + } + + configChan := make(chan dynamic.Message) + go func() { + err := provider.watchKv(context.Background(), configChan, "prefix", make(chan bool, 1)) + require.NoError(t, err) + }() + + select { + case c1 := <-returnedChans: + c1 <- []*store.KVPair{} + <-configChan + close(c1) // WatchTree chans can close due to error + case <-time.After(1 * time.Second): + t.Fatalf("Failed to create a new WatchTree chan") + } + + select { + case c2 := <-returnedChans: + c2 <- []*store.KVPair{} + <-configChan + case <-time.After(1 * time.Second): + t.Fatalf("Failed to create a new WatchTree chan") + } + + select { + case <-configChan: + t.Fatalf("configChan should be empty") + default: + } +} + +func mapToPairs(in map[string]string) []*store.KVPair { + var out []*store.KVPair + for k, v := range in { + out = append(out, &store.KVPair{Key: k, Value: []byte(v)}) + } + return out +} diff --git a/pkg/provider/kv/redis/redis.go b/pkg/provider/kv/redis/redis.go new file mode 100644 index 000000000..10ec299b8 --- /dev/null +++ b/pkg/provider/kv/redis/redis.go @@ -0,0 +1,25 @@ +package redis + +import ( + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/provider" + "github.com/containous/traefik/v2/pkg/provider/kv" +) + +var _ provider.Provider = (*Provider)(nil) + +// Provider holds configurations of the provider. +type Provider struct { + kv.Provider +} + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.Provider.SetDefaults() + p.Endpoints = []string{"127.0.0.1:6379"} +} + +// Init the provider +func (p *Provider) Init() error { + return p.Provider.Init(store.REDIS, "redis") +} diff --git a/pkg/provider/kv/storewrapper.go b/pkg/provider/kv/storewrapper.go new file mode 100644 index 000000000..0c68a3b4f --- /dev/null +++ b/pkg/provider/kv/storewrapper.go @@ -0,0 +1,118 @@ +package kv + +import ( + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/log" +) + +type storeWrapper struct { + store.Store +} + +func (s *storeWrapper) Put(key string, value []byte, options *store.WriteOptions) error { + log.WithoutContext().Debugf("Put: %s", key, string(value)) + + if s.Store == nil { + return nil + } + return s.Store.Put(key, value, options) +} + +func (s *storeWrapper) Get(key string, options *store.ReadOptions) (*store.KVPair, error) { + log.WithoutContext().Debugf("Get: %s", key) + + if s.Store == nil { + return nil, nil + } + return s.Store.Get(key, options) +} + +func (s *storeWrapper) Delete(key string) error { + log.WithoutContext().Debugf("Delete: %s", key) + + if s.Store == nil { + return nil + } + return s.Store.Delete(key) +} + +func (s *storeWrapper) Exists(key string, options *store.ReadOptions) (bool, error) { + log.WithoutContext().Debugf("Exists: %s", key) + + if s.Store == nil { + return true, nil + } + return s.Store.Exists(key, options) +} + +func (s *storeWrapper) Watch(key string, stopCh <-chan struct{}, options *store.ReadOptions) (<-chan *store.KVPair, error) { + log.WithoutContext().Debugf("Watch: %s", key) + + if s.Store == nil { + return nil, nil + } + return s.Store.Watch(key, stopCh, options) +} + +func (s *storeWrapper) WatchTree(directory string, stopCh <-chan struct{}, options *store.ReadOptions) (<-chan []*store.KVPair, error) { + log.WithoutContext().Debugf("WatchTree: %s", directory) + + if s.Store == nil { + return nil, nil + } + return s.Store.WatchTree(directory, stopCh, options) +} + +func (s *storeWrapper) NewLock(key string, options *store.LockOptions) (store.Locker, error) { + log.WithoutContext().Debugf("NewLock: %s", key) + + if s.Store == nil { + return nil, nil + } + return s.Store.NewLock(key, options) +} + +func (s *storeWrapper) List(directory string, options *store.ReadOptions) ([]*store.KVPair, error) { + log.WithoutContext().Debugf("List: %s", directory) + + if s.Store == nil { + return nil, nil + } + return s.Store.List(directory, options) +} + +func (s *storeWrapper) DeleteTree(directory string) error { + log.WithoutContext().Debugf("DeleteTree: %s", directory) + + if s.Store == nil { + return nil + } + return s.Store.DeleteTree(directory) +} + +func (s *storeWrapper) AtomicPut(key string, value []byte, previous *store.KVPair, options *store.WriteOptions) (bool, *store.KVPair, error) { + log.WithoutContext().Debugf("AtomicPut: %s", key, string(value), previous) + + if s.Store == nil { + return true, nil, nil + } + return s.Store.AtomicPut(key, value, previous, options) +} + +func (s *storeWrapper) AtomicDelete(key string, previous *store.KVPair) (bool, error) { + log.WithoutContext().Debugf("AtomicDelete: %s", key, previous) + + if s.Store == nil { + return true, nil + } + return s.Store.AtomicDelete(key, previous) +} + +func (s *storeWrapper) Close() { + log.WithoutContext().Debugf("Close") + + if s.Store == nil { + return + } + s.Store.Close() +} diff --git a/pkg/provider/kv/zk/zk.go b/pkg/provider/kv/zk/zk.go new file mode 100644 index 000000000..26c7d8956 --- /dev/null +++ b/pkg/provider/kv/zk/zk.go @@ -0,0 +1,25 @@ +package zk + +import ( + "github.com/abronan/valkeyrie/store" + "github.com/containous/traefik/v2/pkg/provider" + "github.com/containous/traefik/v2/pkg/provider/kv" +) + +var _ provider.Provider = (*Provider)(nil) + +// Provider holds configurations of the provider. +type Provider struct { + kv.Provider +} + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.Provider.SetDefaults() + p.Endpoints = []string{"127.0.0.1:2181"} +} + +// Init the provider +func (p *Provider) Init() error { + return p.Provider.Init(store.ZK, "zookeeper") +} diff --git a/webui/src/statics/providers/consul.svg b/webui/src/statics/providers/consul.svg new file mode 100644 index 000000000..b9b33d374 --- /dev/null +++ b/webui/src/statics/providers/consul.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/webui/src/statics/providers/etcd.svg b/webui/src/statics/providers/etcd.svg new file mode 100644 index 000000000..3c270f632 --- /dev/null +++ b/webui/src/statics/providers/etcd.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/webui/src/statics/providers/redis.svg b/webui/src/statics/providers/redis.svg new file mode 100644 index 000000000..e0944a282 --- /dev/null +++ b/webui/src/statics/providers/redis.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/webui/src/statics/providers/zookeeper.svg b/webui/src/statics/providers/zookeeper.svg new file mode 100644 index 000000000..0b9f1be63 --- /dev/null +++ b/webui/src/statics/providers/zookeeper.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +