From 1ef93fead7cd937f83956b941f2efd6b9dd0c40f Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Wed, 15 Jul 2020 16:56:03 +0200 Subject: [PATCH] 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 --- docs/content/providers/http.md | 189 +++++++++++++ docs/content/providers/overview.md | 3 +- .../reference/static-configuration/cli-ref.md | 27 ++ .../reference/static-configuration/env-ref.md | 27 ++ .../reference/static-configuration/file.toml | 10 + .../reference/static-configuration/file.yaml | 10 + docs/mkdocs.yml | 1 + integration/fixtures/http/simple.toml | 20 ++ integration/http_test.go | 87 ++++++ integration/integration_test.go | 1 + pkg/config/static/static_config.go | 2 + pkg/provider/aggregator/aggregator.go | 4 + pkg/provider/http/http.go | 172 ++++++++++++ pkg/provider/http/http_test.go | 253 ++++++++++++++++++ webui/src/statics/providers/http.svg | 11 + 15 files changed, 816 insertions(+), 1 deletion(-) create mode 100644 docs/content/providers/http.md create mode 100644 integration/fixtures/http/simple.toml create mode 100644 integration/http_test.go create mode 100644 pkg/provider/http/http.go create mode 100644 pkg/provider/http/http_test.go create mode 100644 webui/src/statics/providers/http.svg diff --git a/docs/content/providers/http.md b/docs/content/providers/http.md new file mode 100644 index 000000000..aae25b2a5 --- /dev/null +++ b/docs/content/providers/http.md @@ -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 +``` diff --git a/docs/content/providers/overview.md b/docs/content/providers/overview.md index 71b4d974f..cb651de64 100644 --- a/docs/content/providers/overview.md +++ b/docs/content/providers/overview.md @@ -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" diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index e4acb8c1e..5bfdc27d9 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -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```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 7056392cc..fe494c67a 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -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```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 50df6eb02..e38fc340c 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -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 diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 65c46210f..179150c6a 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -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 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 4f54c100c..7dcee7420 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -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' diff --git a/integration/fixtures/http/simple.toml b/integration/fixtures/http/simple.toml new file mode 100644 index 000000000..36df1414a --- /dev/null +++ b/integration/fixtures/http/simple.toml @@ -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" diff --git a/integration/http_test.go b/integration/http_test.go new file mode 100644 index 000000000..3846bc52b --- /dev/null +++ b/integration/http_test.go @@ -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 +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 32309349b..98c500143 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -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{}) diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 8ab85aec5..d23402c14 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -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. diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 8cef74235..eab7d9d38 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -73,6 +73,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { p.quietAddProvider(conf.Redis) } + if conf.HTTP != nil { + p.quietAddProvider(conf.HTTP) + } + return p } diff --git a/pkg/provider/http/http.go b/pkg/provider/http/http.go new file mode 100644 index 000000000..a42b3ae1c --- /dev/null +++ b/pkg/provider/http/http.go @@ -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 +} diff --git a/pkg/provider/http/http_test.go b/pkg/provider/http/http_test.go new file mode 100644 index 000000000..3ab0b2fd6 --- /dev/null +++ b/pkg/provider/http/http_test.go @@ -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)) +} diff --git a/webui/src/statics/providers/http.svg b/webui/src/statics/providers/http.svg new file mode 100644 index 000000000..338e1afca --- /dev/null +++ b/webui/src/statics/providers/http.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file