diff --git a/configuration.go b/configuration.go index a04e1d1f22..8cb64ece42 100644 --- a/configuration.go +++ b/configuration.go @@ -332,7 +332,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { var defaultSidecar provider.Sidecar defaultSidecar.Watch = true defaultSidecar.Endpoint = "http://127.0.0.1:7777" - defaultSidecar.Frontend = "sidecar.toml" + defaultSidecar.Filename = "sidecar.toml" defaultSidecar.RefreshConn = flaeg.Duration(60 * time.Second) //default Docker diff --git a/provider/sidecar.go b/provider/sidecar.go index 214c941db3..f6b372e8a4 100644 --- a/provider/sidecar.go +++ b/provider/sidecar.go @@ -1,17 +1,16 @@ package provider import ( + "context" + "fmt" "io/ioutil" "net" "net/http" "os" "path/filepath" - "strconv" "strings" "time" - fsnotify "gopkg.in/fsnotify.v1" - "github.com/BurntSushi/toml" "github.com/Nitro/sidecar/catalog" "github.com/Nitro/sidecar/service" @@ -19,12 +18,24 @@ import ( "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" + fsnotify "gopkg.in/fsnotify.v1" ) const ( - method = "wrr" - weight = 0 - sticky = false + defaultMethod = "wrr" + defaultSticky = false +) + +var ( + // Disable all timeouts for watcher requests + watcherHTTPClient = &http.Client{ + Timeout: 0, + Transport: &http.Transport{ResponseHeaderTimeout: 0}, + } + + sidecarHTTPClient = &http.Client{ + Timeout: 15 * time.Second, + } ) var _ Provider = (*Sidecar)(nil) @@ -33,7 +44,6 @@ var _ Provider = (*Sidecar)(nil) type Sidecar struct { BaseProvider `mapstructure:",squash"` Endpoint string `description:"Sidecar URL"` - Frontend string `description:"Configuration file for frontend"` configurationChan chan<- types.ConfigMessage RefreshConn flaeg.Duration `description:"How often to refresh the connection to Sidecar backend"` connTimer *time.Timer @@ -43,22 +53,31 @@ type callback func(map[string][]*service.Service, error) // Provide allows the provider to provide configurations to traefik // using the given configuration channel. -func (provider *Sidecar) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { +func (provider *Sidecar) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ types.Constraints) error { provider.configurationChan = configurationChan + + // Exit with a bang if we can't reach the Sidecar endpoint + // TODO: On second thought, maybe it's best to continuously log errors in a loop + // since no other provider uses log.Fatal if something goes wrong... + _, err := watcherHTTPClient.Get(provider.Endpoint) + if err != nil { + log.Fatal("Failed to connect to the Sidecar endpoint: ", err) + } + if provider.Watch { safe.Go(func() { - provider.sidecarWatcher() + provider.runSidecarWatcher() }) watcher, err := fsnotify.NewWatcher() if err != nil { - log.Errorln("Error creating file watcher", err) + log.Error("Error creating file watcher: ", err) return err } - file, err := os.Open(provider.Frontend) + file, err := os.Open(provider.Filename) if err != nil { - log.Errorln("Error opening file", err) + log.Error("Error opening file: ", err) return err } defer file.Close() @@ -70,150 +89,171 @@ func (provider *Sidecar) Provide(configurationChan chan<- types.ConfigMessage, p return case event := <-watcher.Events: if strings.Contains(event.Name, file.Name()) { - log.Debug("Sidecar Frontend File event:", event) - states, errState := provider.fetchState() - if errState != nil { - log.Errorln("Error reloading Sidecar config", errState) + log.Debug("Sidecar config file event: ", event) + err = provider.reloadConfig() + if err != nil { + log.Error(err) } - provider.loadSidecarConfig(states.ByService()) } case errWatcher := <-watcher.Errors: - log.Errorln("Watcher event error", errWatcher) + log.Error("Watcher event error: ", errWatcher) } } }) err = watcher.Add(filepath.Dir(file.Name())) if err != nil { - log.Error("Error adding file watcher", err) + log.Error("Error adding file watcher: ", err) return err } + } else { + err := provider.reloadConfig() + if err != nil { + log.Error(err) + } } + + return nil +} + +func (provider *Sidecar) reloadConfig() error { states, err := provider.fetchState() if err != nil { - log.Fatalln("Error reloading Sidecar config", err) + return fmt.Errorf("Error fetching Sidecar state: %s", err) } - err = provider.loadSidecarConfig(states.ByService()) + + err = provider.loadConfig(states.ByService()) if err != nil { - return err + return fmt.Errorf("Error loading Sidecar config: %s", err) } + return nil } -func (provider *Sidecar) constructConfig(sidecarStates map[string][]*service.Service) (*types.Configuration, error) { - log.Infoln("loading sidecar config") - sidecarConfig := types.Configuration{Backends: provider.makeBackends(sidecarStates)} - var err error - sidecarConfig.Frontends, err = provider.makeFrontend() - if err != nil { - return nil, err +func (provider *Sidecar) loadConfig(sidecarStates map[string][]*service.Service) error { + log.Info("Loading sidecar config...") + + config := &types.Configuration{ + Backends: make(map[string]*types.Backend), } - return &sidecarConfig, nil -} -func (provider *Sidecar) loadSidecarConfig(sidecarStates map[string][]*service.Service) error { - conf, err := provider.constructConfig(sidecarStates) - if err != nil { + if _, err := toml.DecodeFile(provider.Filename, config); err != nil { return err } + + // Create backends from Sidecar state data + for serviceName, services := range sidecarStates { + backend, ok := config.Backends[serviceName] + if !ok { + backend = &types.Backend{} + config.Backends[serviceName] = backend + } + + if backend.LoadBalancer == nil { + backend.LoadBalancer = &types.LoadBalancer{Method: defaultMethod, Sticky: defaultSticky} + } + if backend.Servers == nil { + backend.Servers = make(map[string]types.Server) + } + + for _, serv := range services { + if serv.IsAlive() { + ipAddr, err := net.LookupIP(serv.Hostname) + + for i := 0; i < len(serv.Ports); i++ { + // TODO: is there any point to add unreachable hosts? + var hostname string + if err != nil { + log.Warn("Failed to resolve IP address: ", err) + hostname = serv.Hostname + } else { + hostname = ipAddr[0].String() + } + + backend.Servers[serv.Hostname] = types.Server{ + URL: fmt.Sprintf("http://%s:%d", hostname, serv.Ports[i].Port), + } + } + } + } + } + provider.configurationChan <- types.ConfigMessage{ ProviderName: "sidecar", - Configuration: conf, + Configuration: config, } + + log.Info("Finished loading sidecar config") + return nil } -func (provider *Sidecar) sidecarWatcher() error { - //set timeout to be just a bot more than connection refresh interval +func (provider *Sidecar) runSidecarWatcher() { + // Set timeout to be just a bit more than connection refresh interval provider.connTimer = time.NewTimer(time.Duration(provider.RefreshConn)) - tr := &http.Transport{ResponseHeaderTimeout: 0} - client := &http.Client{ - Timeout: 0, - Transport: tr} + log.Debugf("Using %s Sidecar connection refresh interval", provider.RefreshConn) - provider.recycleConn(client, tr) - return nil + for { + // Call a separate function because the defer statement has function scope + provider.sidecarWatcher() + } } -func (provider *Sidecar) recycleConn(client *http.Client, tr *http.Transport) { - var err error - var resp *http.Response - var req *http.Request - for { //use refresh interval to occasionally reconnect to Sidecar in case the stream connection is lost - req, err = http.NewRequest("GET", provider.Endpoint+"/watch", nil) - if err != nil { - log.Errorf("Error creating http request to Sidecar: %s, Error: %s", provider.Endpoint, err) - continue - } - resp, err = client.Do(req) - if err != nil { - log.Errorf("Error connecting to Sidecar: %s, Error: %s", provider.Endpoint, err) - time.Sleep(5 * time.Second) - continue - } - safe.Go(func() { catalog.DecodeStream(resp.Body, provider.callbackLoader) }) +func (provider *Sidecar) sidecarWatcher() { + // Use refresh interval to occasionally reconnect to Sidecar in case the stream connection is lost + req, err := http.NewRequest(http.MethodGet, provider.Endpoint+"/watch", nil) + if err != nil { + log.Errorf("Error creating http request to Sidecar instance '%s': %s", provider.Endpoint, err) + time.Sleep(5 * time.Second) + return + } - //wait on refresh connection timer. If this expires we haven't seen an update in a - //while and should cancel the request, reset the time, and reconnect just in case - <-provider.connTimer.C - provider.connTimer.Reset(time.Duration(provider.RefreshConn)) - tr.CancelRequest(req) + cx, cancel := context.WithCancel(context.Background()) + // Cancel the infinite timeout request automatically after we reset connTimer + defer cancel() + + req = req.WithContext(cx) + + resp, err := watcherHTTPClient.Do(req) + if err != nil { + log.Errorf("Error connecting to Sidecar instance '%s': %s", provider.Endpoint, err) + time.Sleep(5 * time.Second) + return } + defer resp.Body.Close() + + safe.Go(func() { catalog.DecodeStream(resp.Body, provider.callbackLoader) }) + + // Wait on refresh connection timer. If this expires we haven't seen an update in a + // while and should cancel the request, reset the time, and reconnect just in case + <-provider.connTimer.C + provider.connTimer.Reset(time.Duration(provider.RefreshConn)) } func (provider *Sidecar) callbackLoader(sidecarStates map[string][]*service.Service, err error) { //load config regardless - provider.loadSidecarConfig(sidecarStates) + configErr := provider.loadConfig(sidecarStates) + if configErr != nil { + log.Error("Error loading sidecar config: ", err) + } if err != nil { return } - //else reset connection timer + + // Else reset connection timer if !provider.connTimer.Stop() { <-provider.connTimer.C } - provider.connTimer.Reset(time.Duration(provider.RefreshConn)) - return -} -func (provider *Sidecar) makeFrontend() (map[string]*types.Frontend, error) { - configuration := new(types.Configuration) - if _, err := toml.DecodeFile(provider.Frontend, configuration); err != nil { - log.Errorf("Error reading file: %s", err) - return nil, err - } - return configuration.Frontends, nil -} - -func (provider *Sidecar) makeBackends(sidecarStates map[string][]*service.Service) map[string]*types.Backend { - sidecarBacks := make(map[string]*types.Backend) - for serviceName, services := range sidecarStates { - newServers := make(map[string]types.Server) - newBackend := &types.Backend{LoadBalancer: &types.LoadBalancer{Method: method, Sticky: sticky}, - Servers: newServers} - for _, serv := range services { - if serv.IsAlive() { - for i := 0; i < len(serv.Ports); i++ { - ipAddr, err := net.LookupIP(serv.Hostname) - if err != nil { - log.Errorln("Error resolving Ip address, ", err) - newBackend.Servers[serv.Hostname] = types.Server{URL: "http://" + serv.Hostname + ":" + strconv.FormatInt(serv.Ports[i].Port, 10)} - } else { - newBackend.Servers[serv.Hostname] = types.Server{URL: "http://" + ipAddr[0].String() + ":" + strconv.FormatInt(serv.Ports[i].Port, 10)} - } - } - } - } - sidecarBacks[serviceName] = newBackend - } - return sidecarBacks + provider.connTimer.Reset(time.Duration(provider.RefreshConn)) } func (provider *Sidecar) fetchState() (*catalog.ServicesState, error) { - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Get(provider.Endpoint + "/state.json") + resp, err := watcherHTTPClient.Get(provider.Endpoint + "/state.json") if err != nil { return nil, err } + defer resp.Body.Close() bytes, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -224,5 +264,6 @@ func (provider *Sidecar) fetchState() (*catalog.ServicesState, error) { if err != nil { return nil, err } + return state, nil } diff --git a/provider/sidecar_test.go b/provider/sidecar_test.go index 7c9619fbf5..9c500b56f1 100644 --- a/provider/sidecar_test.go +++ b/provider/sidecar_test.go @@ -5,7 +5,6 @@ import ( "net/http" "reflect" "testing" - "time" "github.com/Nitro/sidecar/catalog" "github.com/Nitro/sidecar/service" @@ -15,48 +14,80 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func Test_FetchState(t *testing.T) { - Convey("Verify Fetching State handler", t, func() { - var baseTime = time.Now().UTC().Round(time.Second) - var testPort = service.Port{Type: "tcp", Port: 8000, ServicePort: 8000} - - httpmock.Activate() - defer httpmock.DeactivateAndReset() +var ( + origSidecarHTTPClient = sidecarHTTPClient + origWatcherHTTPClient = watcherHTTPClient + dummyState *catalog.ServicesState +) - httpmock.RegisterResponder("GET", "http://some.dummy.service/state.json", - func(req *http.Request) (*http.Response, error) { +func setup() { + sidecarHTTPClient = http.DefaultClient + watcherHTTPClient = http.DefaultClient - service := service.Service{ID: "007", Name: "api", Hostname: "some-aws-host", - Updated: baseTime, Status: 1, Ports: []service.Port{testPort}} - returnState := catalog.NewServicesState() - returnState.AddServiceEntry(service) - resp, err := httpmock.NewJsonResponse(200, returnState) - if err != nil { - return httpmock.NewStringResponse(500, ""), nil - } - return resp, nil + dummyState = catalog.NewServicesState() + dummyState.AddServiceEntry( + service.Service{ + ID: "007", + Name: "web", + Hostname: "some-aws-host", + Status: 0, + Ports: []service.Port{ + service.Port{ + Type: "tcp", + Port: 9000, + ServicePort: 9000, + }, }, - ) + }, + ) + + httpmock.Activate() + + httpmock.RegisterResponder(http.MethodGet, "http://some.dummy.service", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(200, ""), nil + }, + ) + httpmock.RegisterResponder(http.MethodGet, "http://some.dummy.service/state.json", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, dummyState) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) +} + +func teardown() { + httpmock.DeactivateAndReset() + + sidecarHTTPClient = origSidecarHTTPClient + watcherHTTPClient = origWatcherHTTPClient +} + +func TestSidecarFetchState(t *testing.T) { + setup() + defer teardown() + + Convey("Verify fetching state", t, func() { prov := Sidecar{ Endpoint: "http://some.dummy.service", } - testState, err := prov.fetchState() - testServices := testState.ByService() + receivedState, err := prov.fetchState() + So(err, ShouldBeNil) - compareState := catalog.NewServicesState() - service := &service.Service{ID: "007", Name: "api", Hostname: "some-aws-host", - Updated: baseTime, Status: 1, Ports: []service.Port{testPort}} - compareState.AddServiceEntry(*service) - compareServices := compareState.ByService() + receivedServices := receivedState.ByService() - So(err, ShouldBeNil) - So(reflect.DeepEqual(testServices["api"][0].Ports, compareServices["api"][0].Ports), ShouldBeTrue) - So(testServices["api"][0].Hostname, ShouldEqual, compareServices["api"][0].Hostname) + expectedServices := dummyState.ByService() - compareServices["api"][0].Hostname = "wrong-host" - So(testServices["api"][0].Hostname, ShouldNotEqual, compareServices["api"][0].Hostname) + So(reflect.DeepEqual(receivedServices["web"][0].Ports, expectedServices["web"][0].Ports), ShouldBeTrue) + So(receivedServices["web"][0].Hostname, ShouldEqual, expectedServices["web"][0].Hostname) + + expectedServices["web"][0].Hostname = "wrong-host" + So(receivedServices["web"][0].Hostname, ShouldNotEqual, expectedServices["web"][0].Hostname) prov.Endpoint = "http://yetanother.dummy.service" _, err = prov.fetchState() @@ -64,131 +95,113 @@ func Test_FetchState(t *testing.T) { }) } -func Test_FetchBackend(t *testing.T) { - Convey("Verify Fetching Backend", t, func() { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func TestSidecarLoadConfig(t *testing.T) { + setup() + defer teardown() + Convey("Verify load config", t, func() { + configMsgChan := make(chan types.ConfigMessage) prov := Sidecar{ - Endpoint: "http://some.dummy.service", + BaseProvider: BaseProvider{ + Filename: "testdata/sidecar_config.toml", + }, + Endpoint: "http://some.dummy.service", + configurationChan: configMsgChan, } - httpmock.RegisterResponder("GET", "http://some.dummy.service/state.json", - func(req *http.Request) (*http.Response, error) { - - testPort := service.Port{Type: "tcp", Port: 8000, ServicePort: 8000} - returnState := catalog.NewServicesState() - baseTime := time.Now().UTC().Round(time.Second) - serviceA := service.Service{ID: "007", Name: "web", Hostname: "some-aws-host", - Updated: baseTime.Add(5 * time.Second), Status: 0, Ports: []service.Port{testPort}} - serviceB := service.Service{ID: "008", Name: "api", Hostname: "another-aws-host", - Updated: baseTime, Status: 1} - returnState.AddServiceEntry(serviceA) - returnState.AddServiceEntry(serviceB) - resp, err := httpmock.NewJsonResponse(200, returnState) - if err != nil { - return httpmock.NewStringResponse(500, ""), nil - } - return resp, nil + dummyState.AddServiceEntry( + service.Service{ + ID: "008", + Name: "raster", + Hostname: "another-aws-host", + Status: 1, }, ) + states, err := prov.fetchState() sidecarStates := states.ByService() - backs := prov.makeBackends(sidecarStates) + + configLoaded := make(chan bool) + go func() { + err = prov.loadConfig(sidecarStates) + configLoaded <- true + }() + configMsg := <-configMsgChan + <-configLoaded So(err, ShouldBeNil) - So(backs["web"].LoadBalancer.Method, ShouldEqual, "wrr") - So(backs["web"].Servers["some-aws-host"].URL, ShouldEqual, "http://some-aws-host:8000") - So(backs["api"].Servers["another-aws-host"], ShouldBeZeroValue) - }) -} -func Test_MakeFrontEnd(t *testing.T) { - Convey("Verify Sidecar Frontend Config Loader", t, func() { - httpmock.Activate() - defer httpmock.DeactivateAndReset() + So(configMsg.ProviderName, ShouldEqual, "sidecar") - httpmock.RegisterResponder("GET", "http://some.dummy.service", - func(req *http.Request) (*http.Response, error) { + frontends := configMsg.Configuration.Frontends + backends := configMsg.Configuration.Backends - returnState := catalog.NewServicesState() - resp, err := httpmock.NewJsonResponse(200, returnState) - if err != nil { - return httpmock.NewStringResponse(500, ""), nil - } - return resp, nil - }, - ) - prov := Sidecar{ - Endpoint: "http://some.dummy.service", - } - prov.Watch = true - prov.Frontend = "testdata/sidecar_testdata.toml" - conf, err := prov.makeFrontend() - So(err, ShouldEqual, nil) - So(conf["web"].PassHostHeader, ShouldEqual, true) - So(conf["web"].EntryPoints, ShouldResemble, []string{"http", "https"}) - So(conf["web"].Routes["test_1"].Rule, ShouldEqual, "Host: some-aws-host") - prov.Frontend = "testdata/dummyfile.toml" - _, err = prov.makeFrontend() + So(frontends["web"].PassHostHeader, ShouldEqual, true) + So(frontends["web"].EntryPoints, ShouldResemble, []string{"http", "https"}) + So(frontends["web"].Routes["test_1"].Rule, ShouldEqual, "Host: some-aws-host") + + So(backends, ShouldContainKey, "web") + So(backends, ShouldContainKey, "raster") + So(backends["web"].LoadBalancer.Method, ShouldEqual, "wrr") + So(backends["web"].LoadBalancer.Sticky, ShouldEqual, false) + So(backends["web"].Servers["some-aws-host"].URL, ShouldEqual, "http://some-aws-host:9000") + So(backends["raster"].Servers["another-aws-host"], ShouldBeZeroValue) + + So(backends["web"].MaxConn.Amount, ShouldEqual, 10) + So(backends["web"].MaxConn.ExtractorFunc, ShouldEqual, "request.host") + + prov.Filename = "testdata/dummyfile.toml" + err = prov.loadConfig(sidecarStates) So(err, ShouldNotBeNil) }) } -func Test_SidecarProvider(t *testing.T) { - Convey("Verify Sidecar Provider", t, func() { - httpmock.Activate() - defer httpmock.DeactivateAndReset() - - httpmock.RegisterResponder("GET", "http://some.dummy.service/state.json", - func(req *http.Request) (*http.Response, error) { +func TestSidecarProvider(t *testing.T) { + setup() + defer teardown() - testPort := service.Port{Type: "tcp", Port: 8000, ServicePort: 8000} - returnState := catalog.NewServicesState() - baseTime := time.Now().UTC().Round(time.Second) - serv := service.Service{ID: "007", Name: "web", Hostname: "some-aws-host", - Updated: baseTime.Add(5 * time.Second), Status: 0, Ports: []service.Port{testPort}} - returnState.AddServiceEntry(serv) - resp, err := httpmock.NewJsonResponse(200, returnState) - if err != nil { - return httpmock.NewStringResponse(500, ""), nil - } - return resp, nil - }, - ) + Convey("Verify provider", t, func(c C) { prov := Sidecar{ + BaseProvider: BaseProvider{ + Watch: false, + Filename: "testdata/sidecar_config.toml", + }, Endpoint: "http://some.dummy.service", } - prov.Watch = false - prov.Frontend = "testdata/sidecar_testdata.toml" - - configurationChan := make(chan types.ConfigMessage, 1) - constraints := types.Constraints{} - pool := safe.NewPool(context.Background()) - err := prov.Provide(configurationChan, pool, constraints) - configMsg, _ := <-configurationChan - So(err, ShouldBeNil) + + configMsgChan := make(chan types.ConfigMessage) + provideFinished := make(chan bool) + go func() { + err := prov.Provide(configMsgChan, nil, nil) + + c.So(err, ShouldBeNil) + + provideFinished <- true + }() + configMsg := <-configMsgChan + <-provideFinished + So(configMsg.ProviderName, ShouldEqual, "sidecar") So(configMsg.Configuration.Frontends["web"].Routes["test_1"].Rule, ShouldEqual, "Host: some-aws-host") - So(configMsg.Configuration.Backends["web"].Servers["some-aws-host"].URL, ShouldEndWith, "http://some-aws-host:8000") + So(configMsg.Configuration.Backends["web"].Servers["some-aws-host"].URL, ShouldEqual, "http://some-aws-host:9000") + + So(configMsg.Configuration.Backends["web"].MaxConn.Amount, ShouldEqual, 10) + So(configMsg.Configuration.Backends["web"].MaxConn.ExtractorFunc, ShouldEqual, "request.host") }) } -func Test_SidecarWatcher(t *testing.T) { - Convey("Verify Sidecar Provider", t, func() { - httpmock.Activate() - defer httpmock.DeactivateAndReset() +func TestSidecarWatcher(t *testing.T) { + setup() + defer teardown() - httpmock.RegisterResponder("GET", "http://some.dummy.service/state.json", + Convey("Verify watcher", t, func(c C) { + // Return the watcher mock HTTP response on demand + releaseWatch := make(chan bool) + httpmock.RegisterResponder(http.MethodGet, "http://some.dummy.service/watch", func(req *http.Request) (*http.Response, error) { + <-releaseWatch - testPort := service.Port{Type: "tcp", Port: 9000, ServicePort: 9000} - returnState := catalog.NewServicesState() - baseTime := time.Now().UTC().Round(time.Second) - serv := service.Service{ID: "007", Name: "web", Hostname: "some-aws-host", - Updated: baseTime.Add(5 * time.Second), Status: 0, Ports: []service.Port{testPort}} - returnState.AddServiceEntry(serv) - resp, err := httpmock.NewJsonResponse(200, returnState) + resp, err := httpmock.NewJsonResponse(200, dummyState.ByService()) if err != nil { return httpmock.NewStringResponse(500, ""), nil } @@ -196,34 +209,55 @@ func Test_SidecarWatcher(t *testing.T) { }, ) - httpmock.RegisterResponder("GET", "http://some.dummy.service/watch", - func(req *http.Request) (*http.Response, error) { - - testPort := service.Port{Type: "tcp", Port: 9000, ServicePort: 9000} - returnState := catalog.NewServicesState() - baseTime := time.Now().UTC().Round(time.Second) - serv := service.Service{ID: "007", Name: "web", Hostname: "some-aws-host", - Updated: baseTime.Add(5 * time.Second), Status: 0, Ports: []service.Port{testPort}} - returnState.AddServiceEntry(serv) - resp, err := httpmock.NewJsonResponse(200, returnState.ByService()) - if err != nil { - return httpmock.NewStringResponse(500, ""), nil - } - return resp, nil - }, - ) prov := Sidecar{ + BaseProvider: BaseProvider{ + Watch: true, + Filename: "testdata/sidecar_config.toml", + }, Endpoint: "http://some.dummy.service", } - prov.Watch = true - prov.Frontend = "testdata/sidecar_testdata.toml" - configurationChan := make(chan types.ConfigMessage, 100) - constraints := types.Constraints{} - pool := safe.NewPool(context.Background()) - go prov.Provide(configurationChan, pool, constraints) - configMsg, _ := <-configurationChan + + // We have no way to shut down the provider when it's running in watch mode, + // so just let the unit test close it at the end + configMsgChan := make(chan types.ConfigMessage) + go func() { + err := prov.Provide( + configMsgChan, + safe.NewPool(context.Background()), + nil, + ) + + c.So(err, ShouldBeNil) + }() + + releaseWatch <- true + configMsg := <-configMsgChan + So(configMsg.ProviderName, ShouldEqual, "sidecar") So(configMsg.Configuration.Frontends["web"].Routes["test_1"].Rule, ShouldEqual, "Host: some-aws-host") - So(configMsg.Configuration.Backends["web"].Servers["some-aws-host"].URL, ShouldEndWith, "http://some-aws-host:9000") + So(configMsg.Configuration.Backends["web"].Servers["some-aws-host"].URL, ShouldEqual, "http://some-aws-host:9000") + So(configMsg.Configuration.Backends["web"].MaxConn.Amount, ShouldEqual, 10) + So(configMsg.Configuration.Backends["web"].MaxConn.ExtractorFunc, ShouldEqual, "request.host") + + dummyState.AddServiceEntry( + service.Service{ + ID: "009", + Name: "api", + Hostname: "another-aws-host", + Status: 0, + Ports: []service.Port{ + service.Port{ + Type: "tcp", + Port: 9000, + ServicePort: 9000, + }, + }, + }, + ) + + releaseWatch <- true + configMsg = <-configMsgChan + + So(configMsg.Configuration.Backends["api"].Servers["another-aws-host"].URL, ShouldEqual, "http://another-aws-host:9000") }) } diff --git a/provider/testdata/sidecar_config.toml b/provider/testdata/sidecar_config.toml new file mode 100644 index 0000000000..462941bf04 --- /dev/null +++ b/provider/testdata/sidecar_config.toml @@ -0,0 +1,19 @@ +[frontends] + [frontends.web] + backend = "web" + passHostHeader = true + entrypoints = ["http","https"] + [frontends.web.routes.test_1] + rule = "Host: some-aws-host" + + [frontends.test_service] + backend = "test_service" + entrypoints = ["http","https"] + [frontends.test_service.routes.test_1] + rule = "Host: some-aws-host" + +[backends] + [backends.web] + [backends.web.maxconn] + amount = 10 + extractorfunc = "request.host" \ No newline at end of file diff --git a/provider/testdata/sidecar_testdata.toml b/provider/testdata/sidecar_testdata.toml deleted file mode 100644 index b74b477d74..0000000000 --- a/provider/testdata/sidecar_testdata.toml +++ /dev/null @@ -1,7 +0,0 @@ -[frontends] - [frontends.web] - backend = "web" - passHostHeader = true - entrypoints = ["http","https"] - [frontends.web.routes.test_1] - rule = "Host: some-aws-host"