Add HTTP Provider

* feat: add HTTP provider implementation

* refactor: add SetDefaults and struct tag for the new file parser

* feat: add TLS configuration property

* refactor: rework HTTP provider implementation

* feat: provide config only once if fetched config is unchanged

* style: lint

* ui: add HTTP provider icon

* tests: simplify and fix integration test

* docs: add reference config for file

* docs: move http reference config for file

Co-authored-by: Daniel Tomcej <daniel.tomcej@gmail.com>
This commit is contained in:
Kevin Pollet 2020-07-15 16:56:03 +02:00 committed by GitHub
parent 285ded6e49
commit 1ef93fead7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 816 additions and 1 deletions

View file

@ -0,0 +1,189 @@
# Traefik & HTTP
Provide your [dynamic configuration](./overview.md) via an HTTP(s) endpoint and let Traefik do the rest!
## Routing Configuration
The HTTP provider uses the same configuration as the [File Provider](./file.md) in YAML or JSON format.
## Provider Configuration
### `endpoint`
_Required_
Defines the HTTP(s) endpoint to poll.
```toml tab="File (TOML)"
[providers.http]
endpoint = "http://127.0.0.1:9000/api"
```
```yaml tab="File (YAML)"
providers:
http:
endpoint:
- "http://127.0.0.1:9000/api"
```
```bash tab="CLI"
--providers.http.endpoint=http://127.0.0.1:9000/api
```
### `pollInterval`
_Optional, Default="5s"_
Defines the polling interval.
```toml tab="File (TOML)"
[providers.http]
pollInterval = "5s"
```
```yaml tab="File (YAML)"
providers:
http:
pollInterval: "5s"
```
```bash tab="CLI"
--providers.http.pollInterval=5s
```
### `pollTimeout`
_Optional, Default="5s"_
Defines the polling timeout when connecting to the configured endpoint.
```toml tab="File (TOML)"
[providers.http]
pollTimeout = "5s"
```
```yaml tab="File (YAML)"
providers:
http:
pollTimeout: "5s"
```
```bash tab="CLI"
--providers.http.pollTimeout=5s
```
### `tls`
_Optional_
#### `tls.ca`
Certificate Authority used for the secured connection to the configured Endpoint.
```toml tab="File (TOML)"
[providers.http.tls]
ca = "path/to/ca.crt"
```
```yaml tab="File (YAML)"
providers:
http:
tls:
ca: path/to/ca.crt
```
```bash tab="CLI"
--providers.http.tls.ca=path/to/ca.crt
```
#### `tls.caOptional`
Policy followed for the secured connection with TLS Client Authentication to the configured Endpoint.
Requires `tls.ca` to be defined.
- `true`: VerifyClientCertIfGiven
- `false`: RequireAndVerifyClientCert
- if `tls.ca` is undefined NoClientCert
```toml tab="File (TOML)"
[providers.http.tls]
caOptional = true
```
```yaml tab="File (YAML)"
providers:
http:
tls:
caOptional: true
```
```bash tab="CLI"
--providers.http.tls.caOptional=true
```
#### `tls.cert`
Public certificate used for the secured connection to the configured Endpoint.
```toml tab="File (TOML)"
[providers.http.tls]
cert = "path/to/foo.cert"
key = "path/to/foo.key"
```
```yaml tab="File (YAML)"
providers:
http:
tls:
cert: path/to/foo.cert
key: path/to/foo.key
```
```bash tab="CLI"
--providers.http.tls.cert=path/to/foo.cert
--providers.http.tls.key=path/to/foo.key
```
#### `tls.key`
Private certificate used for the secured connection to the configured Endpoint.
```toml tab="File (TOML)"
[providers.http.tls]
cert = "path/to/foo.cert"
key = "path/to/foo.key"
```
```yaml tab="File (YAML)"
providers:
http:
tls:
cert: path/to/foo.cert
key: path/to/foo.key
```
```bash tab="CLI"
--providers.http.tls.cert=path/to/foo.cert
--providers.http.tls.key=path/to/foo.key
```
#### `tls.insecureSkipVerify`
If `insecureSkipVerify` is `true`, TLS connection to the configured Endpoint accepts any certificate presented by the
server and any host name in that certificate.
```toml tab="File (TOML)"
[providers.http.tls]
insecureSkipVerify = true
```
```yaml tab="File (YAML)"
providers:
http:
tls:
insecureSkipVerify: true
```
```bash tab="CLI"
--providers.http.tls.insecureSkipVerify=true
```

View file

@ -35,9 +35,10 @@ Below is the list of the currently supported providers in Traefik.
| [Rancher](./rancher.md) | Orchestrator | Label |
| [File](./file.md) | Manual | TOML/YAML format |
| [Consul](./consul.md) | KV | KV |
| [etcd](./etcd.md) | KV | KV |
| [Etcd](./etcd.md) | KV | KV |
| [Redis](./redis.md) | KV | KV |
| [ZooKeeper](./zookeeper.md) | KV | KV |
| [HTTP](./http.md) | Manual | JSON format |
!!! info "More Providers"

View file

@ -486,6 +486,33 @@ Load dynamic configuration from a file.
`--providers.file.watch`:
Watch provider. (Default: ```true```)
`--providers.http`:
Enable HTTP backend with default settings. (Default: ```false```)
`--providers.http.endpoint`:
Load configuration from this endpoint.
`--providers.http.pollinterval`:
Polling interval for endpoint. (Default: ```5```)
`--providers.http.polltimeout`:
Polling timeout for endpoint. (Default: ```5```)
`--providers.http.tls.ca`:
TLS CA
`--providers.http.tls.caoptional`:
TLS CA.Optional (Default: ```false```)
`--providers.http.tls.cert`:
TLS cert
`--providers.http.tls.insecureskipverify`:
TLS insecure skip verify (Default: ```false```)
`--providers.http.tls.key`:
TLS key
`--providers.kubernetescrd`:
Enable Kubernetes backend with default settings. (Default: ```false```)

View file

@ -486,6 +486,33 @@ Load dynamic configuration from a file.
`TRAEFIK_PROVIDERS_FILE_WATCH`:
Watch provider. (Default: ```true```)
`TRAEFIK_PROVIDERS_HTTP`:
Enable HTTP backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_HTTP_ENDPOINT`:
Load configuration from this endpoint.
`TRAEFIK_PROVIDERS_HTTP_POLLINTERVAL`:
Polling interval for endpoint. (Default: ```5```)
`TRAEFIK_PROVIDERS_HTTP_POLLTIMEOUT`:
Polling timeout for endpoint. (Default: ```5```)
`TRAEFIK_PROVIDERS_HTTP_TLS_CA`:
TLS CA
`TRAEFIK_PROVIDERS_HTTP_TLS_CAOPTIONAL`:
TLS CA.Optional (Default: ```false```)
`TRAEFIK_PROVIDERS_HTTP_TLS_CERT`:
TLS cert
`TRAEFIK_PROVIDERS_HTTP_TLS_INSECURESKIPVERIFY`:
TLS insecure skip verify (Default: ```false```)
`TRAEFIK_PROVIDERS_HTTP_TLS_KEY`:
TLS key
`TRAEFIK_PROVIDERS_KUBERNETESCRD`:
Enable Kubernetes backend with default settings. (Default: ```false```)

View file

@ -205,6 +205,16 @@
cert = "foobar"
key = "foobar"
insecureSkipVerify = true
[providers.http]
endpoint = "foobar"
pollInterval = 42
pollTimeout = 42
[providers.http.tls]
ca = "foobar"
caOptional = true
cert = "foobar"
key = "foobar"
insecureSkipVerify = true
[api]
insecure = true

View file

@ -225,6 +225,16 @@ providers:
cert: foobar
key: foobar
insecureSkipVerify: true
http:
endpoint: foobar
pollInterval: 42
pollTimeout: 42
tls:
ca: foobar
caOptional: true
cert: foobar
key: foobar
insecureSkipVerify: true
api:
insecure: true
dashboard: true

View file

@ -85,6 +85,7 @@ nav:
- 'Etcd': 'providers/etcd.md'
- 'ZooKeeper': 'providers/zookeeper.md'
- 'Redis': 'providers/redis.md'
- 'HTTP': 'providers/http.md'
- 'Routing & Load Balancing':
- 'Overview': 'routing/overview.md'
- 'EntryPoints': 'routing/entrypoints.md'

View file

@ -0,0 +1,20 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints]
[entryPoints.web]
address = ":8000"
[entryPoints.traefik]
address = ":9090"
[api]
insecure = true
[providers]
[providers.http]
endpoint = "http://127.0.0.1:9000"
pollInterval = "100ms"

87
integration/http_test.go Normal file
View file

@ -0,0 +1,87 @@
package integration
import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"time"
"github.com/containous/traefik/v2/integration/try"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/go-check/check"
checker "github.com/vdemeester/shakers"
)
type HTTPSuite struct{ BaseSuite }
func (s *HTTPSuite) TestSimpleConfiguration(c *check.C) {
cmd, display := s.traefikCmd(withConfigFile("fixtures/http/simple.toml"))
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// Expect a 404 as we configured nothing.
err = try.GetRequest("http://127.0.0.1:8000/", time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
// Provide a configuration, fetched by Traefik provider.
configuration := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"routerHTTP": {
EntryPoints: []string{"web"},
Middlewares: []string{},
Service: "serviceHTTP",
Rule: "PathPrefix(`/`)",
},
},
Services: map[string]*dynamic.Service{
"serviceHTTP": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: boolRef(true),
Servers: []dynamic.Server{
{
URL: "http://bacon:80",
},
},
},
},
},
},
}
configData, err := json.Marshal(configuration)
c.Assert(err, checker.IsNil)
server := startTestServerWithResponse(configData)
defer server.Close()
// Expect configuration to be applied.
err = try.GetRequest("http://127.0.0.1:9090/api/rawdata", 3*time.Second, try.BodyContains("routerHTTP@http", "serviceHTTP@http", "http://bacon:80"))
c.Assert(err, checker.IsNil)
}
func startTestServerWithResponse(response []byte) (ts *httptest.Server) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(response)
})
listener, err := net.Listen("tcp", "127.0.0.1:9000")
if err != nil {
panic(err)
}
ts = &httptest.Server{
Listener: listener,
Config: &http.Server{Handler: handler},
}
ts.Start()
return ts
}
func boolRef(b bool) *bool {
return &b
}

View file

@ -49,6 +49,7 @@ func Test(t *testing.T) {
check.Suite(&HealthCheckSuite{})
check.Suite(&HeadersSuite{})
check.Suite(&HostResolverSuite{})
check.Suite(&HTTPSuite{})
check.Suite(&HTTPSSuite{})
check.Suite(&KeepAliveSuite{})
check.Suite(&LogRotationSuite{})

View file

@ -13,6 +13,7 @@ import (
"github.com/containous/traefik/v2/pkg/provider/docker"
"github.com/containous/traefik/v2/pkg/provider/ecs"
"github.com/containous/traefik/v2/pkg/provider/file"
"github.com/containous/traefik/v2/pkg/provider/http"
"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"
@ -177,6 +178,7 @@ type Providers struct {
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
}
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.

View file

@ -73,6 +73,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
p.quietAddProvider(conf.Redis)
}
if conf.HTTP != nil {
p.quietAddProvider(conf.HTTP)
}
return p
}

172
pkg/provider/http/http.go Normal file
View file

@ -0,0 +1,172 @@
package http
import (
"context"
"fmt"
"hash/fnv"
"io/ioutil"
"net/http"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/config/file"
"github.com/containous/traefik/v2/pkg/job"
"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/provider"
"github.com/containous/traefik/v2/pkg/safe"
"github.com/containous/traefik/v2/pkg/tls"
"github.com/containous/traefik/v2/pkg/types"
)
var _ provider.Provider = (*Provider)(nil)
// Provider is a provider.Provider implementation that queries an HTTP(s) endpoint for a configuration.
type Provider struct {
Endpoint string `description:"Load configuration from this endpoint." json:"endpoint" toml:"endpoint" yaml:"endpoint" export:"true"`
PollInterval types.Duration `description:"Polling interval for endpoint." json:"pollInterval,omitempty" toml:"pollInterval,omitempty" yaml:"pollInterval,omitempty"`
PollTimeout types.Duration `description:"Polling timeout for endpoint." json:"pollTimeout,omitempty" toml:"pollTimeout,omitempty" yaml:"pollTimeout,omitempty"`
TLS *types.ClientTLS `description:"Enable TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
httpClient *http.Client
lastConfigurationHash uint64
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.PollInterval = types.Duration(5 * time.Second)
p.PollTimeout = types.Duration(5 * time.Second)
}
// Init the provider.
func (p *Provider) Init() error {
if p.Endpoint == "" {
return fmt.Errorf("non-empty endpoint is required")
}
if p.PollInterval <= 0 {
return fmt.Errorf("poll interval must be greater than 0")
}
p.httpClient = &http.Client{
Timeout: time.Duration(p.PollTimeout),
}
if p.TLS != nil {
tlsConfig, err := p.TLS.CreateTLSConfig(context.Background())
if err != nil {
return fmt.Errorf("unable to create TLS configuration: %w", err)
}
p.httpClient.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
return nil
}
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "http"))
logger := log.FromContext(ctxLog)
operation := func() error {
ticker := time.NewTicker(time.Duration(p.PollInterval))
defer ticker.Stop()
for {
select {
case <-ticker.C:
configData, err := p.fetchConfigurationData()
if err != nil {
return fmt.Errorf("cannot fetch configuration data: %w", err)
}
fnvHasher := fnv.New64()
_, err = fnvHasher.Write(configData)
if err != nil {
return fmt.Errorf("cannot hash configuration data: %w", err)
}
hash := fnvHasher.Sum64()
if hash == p.lastConfigurationHash {
continue
}
p.lastConfigurationHash = hash
configuration, err := decodeConfiguration(configData)
if err != nil {
return fmt.Errorf("cannot decode configuration data: %w", err)
}
configurationChan <- dynamic.Message{
ProviderName: "http",
Configuration: configuration,
}
case <-routineCtx.Done():
return nil
}
}
}
notify := func(err error, time time.Duration) {
logger.Errorf("Provider connection error %+v, retrying in %s", err, time)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
if err != nil {
logger.Errorf("Cannot connect to server endpoint %+v", err)
}
})
return nil
}
// fetchConfigurationData fetches the configuration data from the configured endpoint.
func (p *Provider) fetchConfigurationData() ([]byte, error) {
res, err := p.httpClient.Get(p.Endpoint)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-ok response code: %d", res.StatusCode)
}
return ioutil.ReadAll(res.Body)
}
// decodeConfiguration decodes and returns the dynamic configuration from the given data.
func decodeConfiguration(data []byte) (*dynamic.Configuration, error) {
configuration := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
Options: make(map[string]tls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
}
err := file.DecodeContent(string(data), ".yaml", configuration)
if err != nil {
return nil, err
}
return configuration, nil
}

View file

@ -0,0 +1,253 @@
package http
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/safe"
"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 TestProvider_Init(t *testing.T) {
tests := []struct {
desc string
endpoint string
pollInterval types.Duration
expErr bool
}{
{
desc: "should return an error if no endpoint is configured",
expErr: true,
},
{
desc: "should return an error if pollInterval is equal to 0",
endpoint: "http://localhost:8080",
expErr: true,
},
{
desc: "should not return an error",
endpoint: "http://localhost:8080",
pollInterval: types.Duration(time.Second),
expErr: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
provider := &Provider{
Endpoint: test.endpoint,
PollInterval: test.pollInterval,
}
err := provider.Init()
if test.expErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestProvider_SetDefaults(t *testing.T) {
provider := &Provider{}
provider.SetDefaults()
assert.Equal(t, provider.PollInterval, types.Duration(5*time.Second))
assert.Equal(t, provider.PollTimeout, types.Duration(5*time.Second))
}
func TestProvider_fetchConfigurationData(t *testing.T) {
tests := []struct {
desc string
handler func(rw http.ResponseWriter, req *http.Request)
expData []byte
expErr bool
}{
{
desc: "should return the fetched configuration data",
expData: []byte("{}"),
handler: func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintf(rw, "{}")
},
},
{
desc: "should return an error if endpoint does not return an OK status code",
expErr: true,
handler: func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusNoContent)
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(test.handler))
defer server.Close()
provider := Provider{
Endpoint: server.URL,
PollInterval: types.Duration(1 * time.Second),
PollTimeout: types.Duration(1 * time.Second),
}
err := provider.Init()
require.NoError(t, err)
configData, err := provider.fetchConfigurationData()
if test.expErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, test.expData, configData)
})
}
}
func TestProvider_decodeConfiguration(t *testing.T) {
tests := []struct {
desc string
configData []byte
expConfig *dynamic.Configuration
expErr bool
}{
{
desc: "should return an error if the configuration data cannot be decoded",
expErr: true,
configData: []byte("{"),
},
{
desc: "should return the decoded dynamic configuration",
configData: []byte("{\"tcp\":{\"routers\":{\"foo\":{}}}}"),
expConfig: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"foo": {},
},
Services: make(map[string]*dynamic.TCPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
Options: make(map[string]tls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
configuration, err := decodeConfiguration(test.configData)
if test.expErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, test.expConfig, configuration)
})
}
}
func TestProvider_Provide(t *testing.T) {
handler := func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintf(rw, "{}")
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
provider := Provider{
Endpoint: server.URL,
PollTimeout: types.Duration(1 * time.Second),
PollInterval: types.Duration(100 * time.Millisecond),
}
err := provider.Init()
require.NoError(t, err)
configurationChan := make(chan dynamic.Message)
expConfiguration := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]tls.Store),
Options: make(map[string]tls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
}
err = provider.Provide(configurationChan, safe.NewPool(context.Background()))
require.NoError(t, err)
timeout := time.After(time.Second)
select {
case configuration := <-configurationChan:
assert.NotNil(t, configuration.Configuration)
assert.Equal(t, expConfiguration, configuration.Configuration)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
}
func TestProvider_ProvideConfigurationOnlyOnceIfUnchanged(t *testing.T) {
handler := func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintf(rw, "{}")
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
provider := Provider{
Endpoint: server.URL + "/endpoint",
PollTimeout: types.Duration(1 * time.Second),
PollInterval: types.Duration(100 * time.Millisecond),
}
err := provider.Init()
require.NoError(t, err)
configurationChan := make(chan dynamic.Message, 10)
err = provider.Provide(configurationChan, safe.NewPool(context.Background()))
require.NoError(t, err)
time.Sleep(time.Second)
assert.Equal(t, 1, len(configurationChan))
}

View file

@ -0,0 +1,11 @@
<svg width="32" height="32"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M18.748 23.93a13.78 13.78 0 002.476-6.88h3.102a8.413 8.413 0 01-5.578 6.88zM7.672 17.05h3.142a13.547 13.547 0 002.463 6.89 8.416 8.416 0 01-5.605-6.89zm5.638-9a13.801 13.801 0 00-2.492 6.9H7.672a8.414 8.414 0 015.638-6.9zm-.377 6.9c.321-3.436 2.079-5.827 3.094-6.933 1.05 1.124 2.78 3.494 3.08 6.933h-6.174zm.001 2.1h6.176c-.321 3.44-2.083 5.833-3.097 6.938a11.57 11.57 0 01-3.079-6.938zm11.393-2.1h-3.1a13.537 13.537 0 00-2.445-6.866 8.409 8.409 0 015.544 6.866zM26.5 16c0-5.78-4.695-10.481-10.47-10.498h-.014L16 5.5C10.21 5.5 5.5 10.211 5.5 16c0 5.79 4.71 10.5 10.5 10.5l.016-.001.005.001.008-.002C21.805 26.482 26.5 21.779 26.5 16z" id="a"/>
</defs>
<g fill-rule="nonzero" fill="none">
<circle fill="#45BBEA" cx="16" cy="16" r="16"/>
<use fill="#FFF" xlink:href="#a"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 918 B