diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index a2f396ef5..637024e8b 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -198,11 +198,13 @@ spec: # "Parameter", etc, to support simpler forms of rule matching, but for now we # only support "Rule". kind: Rule - # Priority disambiguates rules of the same length, for route matching. + # (optional) Priority disambiguates rules of the same length, for route matching. priority: 12 services: - name: whoami port: 80 + # (default 1) A weight used by the weighted round-robin strategy (WRR). + weight: 1 --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index bde9e1c2c..69c40b9f5 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -130,10 +130,10 @@ - "traefik.http.services.service0.loadbalancer.healthcheck.timeout=foobar" - "traefik.http.services.service0.loadbalancer.passhostheader=true" - "traefik.http.services.service0.loadbalancer.responseforwarding.flushinterval=foobar" -- "traefik.http.services.service0.loadbalancer.stickiness=true" -- "traefik.http.services.service0.loadbalancer.stickiness.cookiename=foobar" -- "traefik.http.services.service0.loadbalancer.stickiness.httponlycookie=true" -- "traefik.http.services.service0.loadbalancer.stickiness.securecookie=true" +- "traefik.http.services.service0.loadbalancer.sticky=true" +- "traefik.http.services.service0.loadbalancer.sticky.cookie.name=foobar" +- "traefik.http.services.service0.loadbalancer.sticky.cookie.httponly=true" +- "traefik.http.services.service0.loadbalancer.sticky.cookie.secure=true" - "traefik.http.services.service0.loadbalancer.server.port=foobar" - "traefik.http.services.service0.loadbalancer.server.scheme=foobar" - "traefik.http.services.service1.loadbalancer.healthcheck.headers.name0=foobar" @@ -146,10 +146,10 @@ - "traefik.http.services.service1.loadbalancer.healthcheck.timeout=foobar" - "traefik.http.services.service1.loadbalancer.passhostheader=true" - "traefik.http.services.service1.loadbalancer.responseforwarding.flushinterval=foobar" -- "traefik.http.services.service1.loadbalancer.stickiness=true" -- "traefik.http.services.service1.loadbalancer.stickiness.cookiename=foobar" -- "traefik.http.services.service1.loadbalancer.stickiness.httponlycookie=true" -- "traefik.http.services.service1.loadbalancer.stickiness.securecookie=true" +- "traefik.http.services.service1.loadbalancer.sticky=true" +- "traefik.http.services.service1.loadbalancer.sticky.cookie.name=foobar" +- "traefik.http.services.service1.loadbalancer.sticky.cookie.httponly=true" +- "traefik.http.services.service1.loadbalancer.sticky.cookie.secure=true" - "traefik.http.services.service1.loadbalancer.server.port=foobar" - "traefik.http.services.service1.loadbalancer.server.scheme=foobar" - "traefik.tcp.routers.tcprouter0.entrypoints=foobar, foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 1bc9c258c..1e0a6bfb5 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -35,56 +35,47 @@ main = "foobar" sans = ["foobar", "foobar"] [http.services] - [http.services.Service0] - [http.services.Service0.loadBalancer] + [http.services.Service01] + [http.services.Service01.loadBalancer] passHostHeader = true - [http.services.Service0.loadBalancer.stickiness] - cookieName = "foobar" - secureCookie = true - httpOnlyCookie = true + [http.services.Service01.loadBalancer.sticky] + [http.services.Service01.loadBalancer.sticky.cookie] + name = "foobar" + secure = true + httpOnly = true - [[http.services.Service0.loadBalancer.servers]] + [[http.services.Service01.loadBalancer.servers]] url = "foobar" - [[http.services.Service0.loadBalancer.servers]] + [[http.services.Service01.loadBalancer.servers]] url = "foobar" - [http.services.Service0.loadBalancer.healthCheck] + [http.services.Service01.loadBalancer.healthCheck] scheme = "foobar" path = "foobar" port = 42 interval = "foobar" timeout = "foobar" hostname = "foobar" - [http.services.Service0.loadBalancer.healthCheck.headers] + [http.services.Service01.loadBalancer.healthCheck.headers] name0 = "foobar" name1 = "foobar" - [http.services.Service0.loadBalancer.responseForwarding] + [http.services.Service01.loadBalancer.responseForwarding] flushInterval = "foobar" - [http.services.Service1] - [http.services.Service1.loadBalancer] - passHostHeader = true - [http.services.Service1.loadBalancer.stickiness] - cookieName = "foobar" - secureCookie = true - httpOnlyCookie = true + [http.services.Service02] + [http.services.Service02.weighted] - [[http.services.Service1.loadBalancer.servers]] - url = "foobar" + [[http.services.Service02.weighted.services]] + name = "foobar" + weight = 42 - [[http.services.Service1.loadBalancer.servers]] - url = "foobar" - [http.services.Service1.loadBalancer.healthCheck] - scheme = "foobar" - path = "foobar" - port = 42 - interval = "foobar" - timeout = "foobar" - hostname = "foobar" - [http.services.Service1.loadBalancer.healthCheck.headers] - name0 = "foobar" - name1 = "foobar" - [http.services.Service1.loadBalancer.responseForwarding] - flushInterval = "foobar" + [[http.services.Service02.weighted.services]] + name = "foobar" + weight = 42 + [http.services.Service02.weighted.sticky] + [http.services.Service02.weighted.sticky.cookie] + name = "foobar" + secure = true + httpOnly = true [http.middlewares] [http.middlewares.Middleware00] [http.middlewares.Middleware00.addPrefix] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index a789b6d0d..bea253fdc 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -45,34 +45,13 @@ http: - foobar - foobar services: - Service0: + Service01: loadBalancer: - stickiness: - cookieName: foobar - secureCookie: true - httpOnlyCookie: true - servers: - - url: foobar - - url: foobar - healthCheck: - scheme: foobar - path: foobar - port: 42 - interval: foobar - timeout: foobar - hostname: foobar - headers: - name0: foobar - name1: foobar - passHostHeader: true - responseForwarding: - flushInterval: foobar - Service1: - loadBalancer: - stickiness: - cookieName: foobar - secureCookie: true - httpOnlyCookie: true + sticky: + cookie: + name: foobar + secure: true + httpOnly: true servers: - url: foobar - url: foobar @@ -89,6 +68,18 @@ http: passHostHeader: true responseForwarding: flushInterval: foobar + Service02: + weighted: + services: + - name: foobar + weight: 42 + - name: foobar + weight: 42 + sticky: + cookie: + name: foobar + secure: true + httpOnly: true middlewares: Middleware00: addPrefix: diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index a3419d75e..6c2432fdb 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -130,10 +130,10 @@ "traefik.http.services.service0.loadbalancer.healthcheck.timeout": "foobar", "traefik.http.services.service0.loadbalancer.passhostheader": "true", "traefik.http.services.service0.loadbalancer.responseforwarding.flushinterval": "foobar", -"traefik.http.services.service0.loadbalancer.stickiness": "true", -"traefik.http.services.service0.loadbalancer.stickiness.cookiename": "foobar", -"traefik.http.services.service0.loadbalancer.stickiness.httponlycookie": "true", -"traefik.http.services.service0.loadbalancer.stickiness.securecookie": "true", +"traefik.http.services.service0.loadbalancer.sticky": "true", +"traefik.http.services.service0.loadbalancer.sticky.cookie.name": "foobar", +"traefik.http.services.service0.loadbalancer.sticky.cookie.httponly": "true", +"traefik.http.services.service0.loadbalancer.sticky.cookie.secure": "true", "traefik.http.services.service0.loadbalancer.server.port": "foobar", "traefik.http.services.service0.loadbalancer.server.scheme": "foobar", "traefik.http.services.service1.loadbalancer.healthcheck.headers.name0": "foobar", @@ -146,10 +146,10 @@ "traefik.http.services.service1.loadbalancer.healthcheck.timeout": "foobar", "traefik.http.services.service1.loadbalancer.passhostheader": "true", "traefik.http.services.service1.loadbalancer.responseforwarding.flushinterval": "foobar", -"traefik.http.services.service1.loadbalancer.stickiness": "true", -"traefik.http.services.service1.loadbalancer.stickiness.cookiename": "foobar", -"traefik.http.services.service1.loadbalancer.stickiness.httponlycookie": "true", -"traefik.http.services.service1.loadbalancer.stickiness.securecookie": "true", +"traefik.http.services.service1.loadbalancer.sticky": "true", +"traefik.http.services.service1.loadbalancer.sticky.cookie.name": "foobar", +"traefik.http.services.service1.loadbalancer.sticky.cookie.secure": "true", +"traefik.http.services.service1.loadbalancer.sticky.cookie.httponly": "true", "traefik.http.services.service1.loadbalancer.server.port": "foobar", "traefik.http.services.service1.loadbalancer.server.scheme": "foobar", "traefik.tcp.routers.tcprouter0.entrypoints": "foobar, foobar", diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index cef456c93..5bc1fa05b 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -54,13 +54,7 @@ The `Services` are responsible for configuring how to reach the actual services ## Configuring HTTP Services -### General - -Currently, `LoadBalancer` is the only supported kind of HTTP `Service` (see below). -However, since Traefik is an ever evolving project, other kind of HTTP Services will be available in the future, -reason why you have to specify it. - -### Load Balancer +### Servers Load Balancer The load balancers are able to load balance the requests between multiple instances of your programs. @@ -161,7 +155,7 @@ On subsequent requests, the client is forwarded to the same server. ```toml tab="TOML" [http.services] [http.services.my-service] - [http.services.my-service.loadBalancer.stickiness] + [http.services.my-service.loadBalancer.sticky.cookie] ``` ```yaml tab="YAML" @@ -169,18 +163,19 @@ On subsequent requests, the client is forwarded to the same server. services: my-service: loadBalancer: - stickiness: {} + sticky: + cookie: {} ``` -??? example "Adding Stickiness with a Custom Cookie Name" +??? example "Adding Stickiness with custom Options" ```toml tab="TOML" [http.services] [http.services.my-service] - [http.services.my-service.loadBalancer.stickiness] - cookieName = "my_stickiness_cookie_name" - secureCookie = true - httpOnlyCookie = true + [http.services.my-service.loadBalancer.sticky.cookie] + name = "my_sticky_cookie_name" + secure = true + httpOnly = true ``` ```yaml tab="YAML" @@ -188,10 +183,11 @@ On subsequent requests, the client is forwarded to the same server. services: my-service: loadBalancer: - stickiness: - cookieName: my_stickiness_cookie_name - secureCookie: true - httpOnlyCookie: true + sticky: + cookie: + name: my_sticky_cookie_name + secure: true + httpOnly: true ``` #### Health Check @@ -306,6 +302,57 @@ Below are the available options for the health check mechanism: My-Header: bar ``` +### Weighted Round Robin (service) + +The WRR is able to load balance the requests between multiple services based on weights. + +This strategy is only available to load balance between [services](./index.md) and not between [servers](./index.md#servers). + +This strategy can be defined only with [File](../../providers/file.md). + +```toml tab="TOML" +[http.services] + [http.services.canary] + [[http.services.canary.weighted.services]] + name = "appv1" + weight = 3 + [[http.services.canary.weighted.services]] + name = "appv2" + weight = 1 + + [http.services.appv1] + [http.services.appv1.loadBalancer] + [[http.services.appv1.loadBalancer.servers]] + url = "http://private-ip-server-1/" + + [http.services.appv2] + [http.services.appv2.loadBalancer] + [[http.services.appv2.loadBalancer.servers]] + url = "http://private-ip-server-2/" +``` + +```yaml tab="YAML" +http: + services: + canary: + weighted: + services: + - name: appv1 + weight: 3 + - name: appv2 + weight: 1 + + appv1: + loadBalancer: + servers: + - url: "http://private-ip-server-1/" + + appv2: + loadBalancer: + servers: + - url: "http://private-ip-server-2/" +``` + ## Configuring TCP Services ### General diff --git a/integration/fixtures/tcp/multi-tls-options.toml b/integration/fixtures/tcp/multi-tls-options.toml index 9aca38006..b36dae76a 100644 --- a/integration/fixtures/tcp/multi-tls-options.toml +++ b/integration/fixtures/tcp/multi-tls-options.toml @@ -34,7 +34,6 @@ [tcp.services.whoami-no-cert] [tcp.services.whoami-no-cert.loadBalancer] - method = "wrr" [[tcp.services.whoami-no-cert.loadBalancer.servers]] address = "localhost:8083" diff --git a/integration/fixtures/wrr.toml b/integration/fixtures/wrr.toml new file mode 100644 index 000000000..ad63ceb0d --- /dev/null +++ b/integration/fixtures/wrr.toml @@ -0,0 +1,38 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[api] + +[log] + level = "DEBUG" + +[entryPoints] + + [entryPoints.web] + address = ":8000" + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router] + service = "wrr" + rule = "Path(`/whoami`)" + +[http.services] + [[http.services.wrr.weighted.services]] + name = "service1" + weight = 3 + [[http.services.wrr.weighted.services]] + name = "service2" + + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server1 }}" + [http.services.service2.loadBalancer] + [[http.services.service2.loadBalancer.servers]] + url = "{{ .Server2 }}" + diff --git a/integration/fixtures/wrr_sticky.toml b/integration/fixtures/wrr_sticky.toml new file mode 100644 index 000000000..4181dde54 --- /dev/null +++ b/integration/fixtures/wrr_sticky.toml @@ -0,0 +1,41 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[api] + +[log] + level = "DEBUG" + +[entryPoints] + + [entryPoints.web] + address = ":8000" + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router] + service = "wrr" + rule = "Path(`/whoami`)" + +[http.services] + [http.services.wrr.weighted.sticky.cookie] + name = "test" + + [[http.services.wrr.weighted.services]] + name = "service1" + weight = 3 + [[http.services.wrr.weighted.services]] + name = "service2" + weight = 1 + + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server1 }}" + [http.services.service2.loadBalancer] + [[http.services.service2.loadBalancer.servers]] + url = "{{ .Server2 }}" diff --git a/integration/rest_test.go b/integration/rest_test.go index 3aac50fd0..692542c9f 100644 --- a/integration/rest_test.go +++ b/integration/rest_test.go @@ -51,7 +51,7 @@ func (s *RestSuite) TestSimpleConfiguration(c *check.C) { }, Services: map[string]*dynamic.Service{ "service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://" + s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress + ":80", diff --git a/integration/simple_test.go b/integration/simple_test.go index a004ffd19..7105f2482 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -563,7 +564,7 @@ func (s *SimpleSuite) TestServiceConfigErrors(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains(`["the service \"service1@file\" doesn't have any load balancer"]`)) + err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains(`["the service \"service1@file\" does not have any type defined"]`)) c.Assert(err, checker.IsNil) err = try.GetRequest("http://127.0.0.1:8080/api/http/services/service1@file", 1000*time.Millisecond, try.BodyContains(`"status":"disabled"`)) @@ -572,3 +573,101 @@ func (s *SimpleSuite) TestServiceConfigErrors(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/http/services/service2@file", 1000*time.Millisecond, try.BodyContains(`"status":"enabled"`)) c.Assert(err, checker.IsNil) } + +func (s *SimpleSuite) TestWRR(c *check.C) { + s.createComposeProject(c, "base") + s.composeProject.Start(c) + + server1 := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress + server2 := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress + + file := s.adaptFile(c, "fixtures/wrr.toml", struct { + Server1 string + Server2 string + }{Server1: "http://" + server1, Server2: "http://" + server2}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("service1", "service2")) + c.Assert(err, checker.IsNil) + + repartition := map[string]int{} + for i := 0; i < 4; i++ { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + c.Assert(err, checker.IsNil) + + response, err := http.DefaultClient.Do(req) + c.Assert(err, checker.IsNil) + c.Assert(response.StatusCode, checker.Equals, http.StatusOK) + + body, err := ioutil.ReadAll(response.Body) + c.Assert(err, checker.IsNil) + + if strings.Contains(string(body), server1) { + repartition[server1]++ + } + if strings.Contains(string(body), server2) { + repartition[server2]++ + } + } + + c.Assert(repartition[server1], checker.Equals, 3) + c.Assert(repartition[server2], checker.Equals, 1) +} + +func (s *SimpleSuite) TestWRRSticky(c *check.C) { + s.createComposeProject(c, "base") + s.composeProject.Start(c) + + server1 := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress + server2 := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress + + file := s.adaptFile(c, "fixtures/wrr_sticky.toml", struct { + Server1 string + Server2 string + }{Server1: "http://" + server1, Server2: "http://" + server2}) + defer os.Remove(file) + + cmd, output := s.traefikCmd(withConfigFile(file)) + defer output(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("service1", "service2")) + c.Assert(err, checker.IsNil) + + repartition := map[string]int{} + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + c.Assert(err, checker.IsNil) + for i := 0; i < 4; i++ { + + response, err := http.DefaultClient.Do(req) + c.Assert(err, checker.IsNil) + c.Assert(response.StatusCode, checker.Equals, http.StatusOK) + + for _, cookie := range response.Cookies() { + req.AddCookie(cookie) + } + + body, err := ioutil.ReadAll(response.Body) + c.Assert(err, checker.IsNil) + + if strings.Contains(string(body), server1) { + repartition[server1]++ + } + if strings.Contains(string(body), server2) { + repartition[server2]++ + } + } + + c.Assert(repartition[server1], checker.Equals, 4) + c.Assert(repartition[server2], checker.Equals, 0) +} diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go index ea89966a1..57524e675 100644 --- a/pkg/api/handler_http_test.go +++ b/pkg/api/handler_http_test.go @@ -203,7 +203,7 @@ func TestHandler_HTTP(t *testing.T) { "bar@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -219,7 +219,7 @@ func TestHandler_HTTP(t *testing.T) { "baz@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.2", @@ -248,7 +248,7 @@ func TestHandler_HTTP(t *testing.T) { "bar@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -264,7 +264,7 @@ func TestHandler_HTTP(t *testing.T) { "baz@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.2", @@ -280,7 +280,7 @@ func TestHandler_HTTP(t *testing.T) { "test@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.3", @@ -309,7 +309,7 @@ func TestHandler_HTTP(t *testing.T) { "bar@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -337,7 +337,7 @@ func TestHandler_HTTP(t *testing.T) { "bar@myprovider": func() *runtime.ServiceInfo { si := &runtime.ServiceInfo{ Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index c259de2a7..e8eaa7c92 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -55,7 +55,7 @@ func TestHandler_Overview(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{{URL: "http://127.0.0.1"}}, }, }, @@ -63,7 +63,7 @@ func TestHandler_Overview(t *testing.T) { }, "bar-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{{URL: "http://127.0.0.1"}}, }, }, @@ -71,7 +71,7 @@ func TestHandler_Overview(t *testing.T) { }, "fii-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{{URL: "http://127.0.0.1"}}, }, }, diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go index 2971f0354..7b506cd6c 100644 --- a/pkg/api/handler_test.go +++ b/pkg/api/handler_test.go @@ -37,7 +37,7 @@ func TestHandler_RawData(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index 017d1db0d..748cf6497 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -404,8 +404,8 @@ [http.services.Service0] [http.services.Service0.loadBalancer] passHostHeader = true - [http.services.Service0.loadBalancer.stickiness] - cookieName = "foobar" + [http.services.Service0.loadBalancer.sticky.cookie] + name = "foobar" [[http.services.Service0.loadBalancer.servers]] url = "foobar" diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 6bd05ba00..2b855d32f 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -19,7 +19,8 @@ type HTTPConfiguration struct { // Service holds a service configuration (can only be of one type at the same time). type Service struct { - LoadBalancer *LoadBalancerService `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"` + LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"` + Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-"` } // +k8s:deepcopy-gen=true @@ -45,9 +46,47 @@ type RouterTLSConfig struct { // +k8s:deepcopy-gen=true -// LoadBalancerService holds the LoadBalancerService configuration. -type LoadBalancerService struct { - Stickiness *Stickiness `json:"stickiness,omitempty" toml:"stickiness,omitempty" yaml:"stickiness,omitempty" label:"allowEmpty"` +// WeightedRoundRobin is a weighted round robin load-balancer of services. +type WeightedRoundRobin struct { + Services []WRRService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty"` + Sticky *Sticky `json:"sticky,omitempty" toml:"sticky,omitempty" yaml:"sticky,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// WRRService is a reference to a service load-balanced with weighted round robin. +type WRRService struct { + Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"` + Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty"` +} + +// SetDefaults Default values for a ServersLoadBalancer. +func (w *WRRService) SetDefaults() { + defaultWeight := 1 + w.Weight = &defaultWeight +} + +// +k8s:deepcopy-gen=true + +// Sticky holds the sticky configuration. +type Sticky struct { + Cookie *Cookie `json:"cookie,omitempty" toml:"cookie,omitempty" yaml:"cookie,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// Cookie holds the sticky configuration based on cookie. +type Cookie struct { + Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"` + Secure bool `json:"secure,omitempty" toml:"secure,omitempty" yaml:"secure,omitempty"` + HTTPOnly bool `json:"httpOnly,omitempty" toml:"httpOnly,omitempty" yaml:"httpOnly,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// ServersLoadBalancer holds the ServersLoadBalancer configuration. +type ServersLoadBalancer struct { + Sticky *Sticky `json:"sticky,omitempty" toml:"sticky,omitempty" yaml:"sticky,omitempty" label:"allowEmpty"` Servers []Server `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server"` HealthCheck *HealthCheck `json:"healthCheck,omitempty" toml:"healthCheck,omitempty" yaml:"healthCheck,omitempty"` PassHostHeader bool `json:"passHostHeader" toml:"passHostHeader" yaml:"passHostHeader"` @@ -55,7 +94,7 @@ type LoadBalancerService struct { } // Mergeable tells if the given service is mergeable. -func (l *LoadBalancerService) Mergeable(loadBalancer *LoadBalancerService) bool { +func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool { savedServers := l.Servers defer func() { l.Servers = savedServers @@ -71,8 +110,8 @@ func (l *LoadBalancerService) Mergeable(loadBalancer *LoadBalancerService) bool return reflect.DeepEqual(l, loadBalancer) } -// SetDefaults Default values for a LoadBalancerService. -func (l *LoadBalancerService) SetDefaults() { +// SetDefaults Default values for a ServersLoadBalancer. +func (l *ServersLoadBalancer) SetDefaults() { l.PassHostHeader = true } @@ -85,15 +124,6 @@ type ResponseForwarding struct { // +k8s:deepcopy-gen=true -// Stickiness holds the stickiness configuration. -type Stickiness struct { - CookieName string `json:"cookieName,omitempty" toml:"cookieName,omitempty" yaml:"cookieName,omitempty"` - SecureCookie bool `json:"secureCookie,omitempty" toml:"secureCookie,omitempty" yaml:"secureCookie,omitempty"` - HTTPOnlyCookie bool `json:"httpOnlyCookie,omitempty" toml:"httpOnlyCookie,omitempty" yaml:"httpOnlyCookie,omitempty"` -} - -// +k8s:deepcopy-gen=true - // Server holds the server configuration. type Server struct { URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"` diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index 3e1c1e826..e3e42ab68 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -45,7 +45,7 @@ type RouterTCPTLSConfig struct { // TCPLoadBalancerService holds the LoadBalancerService configuration. type TCPLoadBalancerService struct { - Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" label-slice-as-struct:"server"` + Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server"` } // Mergeable tells if the given service is mergeable. diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 09f24794e..454015d9c 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -247,6 +247,22 @@ func (in Configurations) DeepCopy() Configurations { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cookie) DeepCopyInto(out *Cookie) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cookie. +func (in *Cookie) DeepCopy() *Cookie { + if in == nil { + return nil + } + out := new(Cookie) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DigestAuth) DeepCopyInto(out *DigestAuth) { *out = *in @@ -508,42 +524,6 @@ func (in *IPWhiteList) DeepCopy() *IPWhiteList { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LoadBalancerService) DeepCopyInto(out *LoadBalancerService) { - *out = *in - if in.Stickiness != nil { - in, out := &in.Stickiness, &out.Stickiness - *out = new(Stickiness) - **out = **in - } - if in.Servers != nil { - in, out := &in.Servers, &out.Servers - *out = make([]Server, len(*in)) - copy(*out, *in) - } - if in.HealthCheck != nil { - in, out := &in.HealthCheck, &out.HealthCheck - *out = new(HealthCheck) - (*in).DeepCopyInto(*out) - } - if in.ResponseForwarding != nil { - in, out := &in.ResponseForwarding, &out.ResponseForwarding - *out = new(ResponseForwarding) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerService. -func (in *LoadBalancerService) DeepCopy() *LoadBalancerService { - if in == nil { - return nil - } - out := new(LoadBalancerService) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MaxConn) DeepCopyInto(out *MaxConn) { *out = *in @@ -954,12 +934,53 @@ func (in *Server) DeepCopy() *Server { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServersLoadBalancer) DeepCopyInto(out *ServersLoadBalancer) { + *out = *in + if in.Sticky != nil { + in, out := &in.Sticky, &out.Sticky + *out = new(Sticky) + (*in).DeepCopyInto(*out) + } + if in.Servers != nil { + in, out := &in.Servers, &out.Servers + *out = make([]Server, len(*in)) + copy(*out, *in) + } + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(HealthCheck) + (*in).DeepCopyInto(*out) + } + if in.ResponseForwarding != nil { + in, out := &in.ResponseForwarding, &out.ResponseForwarding + *out = new(ResponseForwarding) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServersLoadBalancer. +func (in *ServersLoadBalancer) DeepCopy() *ServersLoadBalancer { + if in == nil { + return nil + } + out := new(ServersLoadBalancer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Service) DeepCopyInto(out *Service) { *out = *in if in.LoadBalancer != nil { in, out := &in.LoadBalancer, &out.LoadBalancer - *out = new(LoadBalancerService) + *out = new(ServersLoadBalancer) + (*in).DeepCopyInto(*out) + } + if in.Weighted != nil { + in, out := &in.Weighted, &out.Weighted + *out = new(WeightedRoundRobin) (*in).DeepCopyInto(*out) } return @@ -976,17 +997,22 @@ func (in *Service) DeepCopy() *Service { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Stickiness) DeepCopyInto(out *Stickiness) { +func (in *Sticky) DeepCopyInto(out *Sticky) { *out = *in + if in.Cookie != nil { + in, out := &in.Cookie, &out.Cookie + *out = new(Cookie) + **out = **in + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stickiness. -func (in *Stickiness) DeepCopy() *Stickiness { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sticky. +func (in *Sticky) DeepCopy() *Sticky { if in == nil { return nil } - out := new(Stickiness) + out := new(Sticky) in.DeepCopyInto(out) return out } @@ -1265,3 +1291,45 @@ func (in Users) DeepCopy() Users { in.DeepCopyInto(out) return *out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WRRService) DeepCopyInto(out *WRRService) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WRRService. +func (in *WRRService) DeepCopy() *WRRService { + if in == nil { + return nil + } + out := new(WRRService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightedRoundRobin) DeepCopyInto(out *WeightedRoundRobin) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]WRRService, len(*in)) + copy(*out, *in) + } + if in.Sticky != nil { + in, out := &in.Sticky, &out.Sticky + *out = new(Sticky) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightedRoundRobin. +func (in *WeightedRoundRobin) DeepCopy() *WeightedRoundRobin { + if in == nil { + return nil + } + out := new(WeightedRoundRobin) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/config/file/fixtures/sample.toml b/pkg/config/file/fixtures/sample.toml index c0050f7db..94dd9e694 100644 --- a/pkg/config/file/fixtures/sample.toml +++ b/pkg/config/file/fixtures/sample.toml @@ -396,8 +396,8 @@ [http.services.Service0] [http.services.Service0.loadBalancer] passHostHeader = true - [http.services.Service0.loadBalancer.stickiness] - cookieName = "foobar" + [http.services.Service0.loadBalancer.sticky.cookie] + name = "foobar" [[http.services.Service0.loadBalancer.servers]] url = "foobar" diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index edeca179e..0edc8bc31 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -142,8 +142,8 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.services.Service0.loadbalancer.responseforwarding.flushinterval": "foobar", "traefik.http.services.Service0.loadbalancer.server.scheme": "foobar", "traefik.http.services.Service0.loadbalancer.server.port": "8080", - "traefik.http.services.Service0.loadbalancer.stickiness.cookiename": "foobar", - "traefik.http.services.Service0.loadbalancer.stickiness.securecookie": "true", + "traefik.http.services.Service0.loadbalancer.sticky.cookie.name": "foobar", + "traefik.http.services.Service0.loadbalancer.sticky.cookie.secure": "true", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.hostname": "foobar", @@ -156,8 +156,8 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.services.Service1.loadbalancer.responseforwarding.flushinterval": "foobar", "traefik.http.services.Service1.loadbalancer.server.scheme": "foobar", "traefik.http.services.Service1.loadbalancer.server.port": "8080", - "traefik.http.services.Service1.loadbalancer.stickiness": "false", - "traefik.http.services.Service1.loadbalancer.stickiness.cookiename": "fui", + "traefik.http.services.Service1.loadbalancer.sticky": "false", + "traefik.http.services.Service1.loadbalancer.sticky.cookie.name": "fui", "traefik.tcp.routers.Router0.rule": "foobar", "traefik.tcp.routers.Router0.entrypoints": "foobar, fiibar", "traefik.tcp.routers.Router0.service": "foobar", @@ -510,11 +510,13 @@ func TestDecodeConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "Service0": { - LoadBalancer: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{ - CookieName: "foobar", - SecureCookie: true, - HTTPOnlyCookie: false, + LoadBalancer: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "foobar", + Secure: true, + HTTPOnly: false, + }, }, Servers: []dynamic.Server{ { @@ -541,7 +543,7 @@ func TestDecodeConfiguration(t *testing.T) { }, }, "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { Scheme: "foobar", @@ -908,10 +910,12 @@ func TestEncodeConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "Service0": { - LoadBalancer: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{ - CookieName: "foobar", - HTTPOnlyCookie: true, + LoadBalancer: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "foobar", + HTTPOnly: true, + }, }, Servers: []dynamic.Server{ { @@ -938,7 +942,7 @@ func TestEncodeConfiguration(t *testing.T) { }, }, "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { Scheme: "foobar", @@ -1101,9 +1105,9 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Services.Service0.LoadBalancer.ResponseForwarding.FlushInterval": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.server.Port": "8080", "traefik.HTTP.Services.Service0.LoadBalancer.server.Scheme": "foobar", - "traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.CookieName": "foobar", - "traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.HTTPOnlyCookie": "true", - "traefik.HTTP.Services.Service0.LoadBalancer.Stickiness.SecureCookie": "false", + "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.Name": "foobar", + "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.HTTPOnly": "true", + "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.Secure": "false", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name0": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname": "foobar", diff --git a/pkg/config/runtime/runtime_test.go b/pkg/config/runtime/runtime_test.go index d220abe84..b2cbc6238 100644 --- a/pkg/config/runtime/runtime_test.go +++ b/pkg/config/runtime/runtime_test.go @@ -43,7 +43,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ {URL: "http://127.0.0.1:8085"}, {URL: "http://127.0.0.1:8086"}, @@ -75,7 +75,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ {URL: "http://127.0.0.1"}, }, @@ -149,7 +149,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:8085", @@ -167,7 +167,7 @@ func TestPopulateUsedBy(t *testing.T) { }, "bar-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:8087", @@ -222,7 +222,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -293,7 +293,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -337,7 +337,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -389,7 +389,7 @@ func TestPopulateUsedBy(t *testing.T) { Services: map[string]*runtime.ServiceInfo{ "foo-service@myprovider": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index 63858c17a..aa5aaf376 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -99,7 +99,7 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock if len(configuration.Services) == 0 { configuration.Services = make(map[string]*dynamic.Service) - lb := &dynamic.LoadBalancerService{} + lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() configuration.Services[serviceName] = &dynamic.Service{ LoadBalancer: lb, @@ -171,7 +171,7 @@ func (p *Provider) addServerTCP(ctx context.Context, container dockerData, loadB return nil } -func (p *Provider) addServer(ctx context.Context, container dockerData, loadBalancer *dynamic.LoadBalancerService) error { +func (p *Provider) addServer(ctx context.Context, container dockerData, loadBalancer *dynamic.ServersLoadBalancer) error { serverPort := getLBServerPort(loadBalancer) ip, port, err := p.getIPPort(ctx, container, serverPort) if err != nil { @@ -291,7 +291,7 @@ func (p *Provider) getPortBinding(container dockerData, serverPort string) (*nat return nil, fmt.Errorf("unable to find the external IP:Port for the container %q", container.Name) } -func getLBServerPort(loadBalancer *dynamic.LoadBalancerService) string { +func getLBServerPort(loadBalancer *dynamic.ServersLoadBalancer) string { if loadBalancer != nil && len(loadBalancer.Servers) > 0 { return loadBalancer.Servers[0].Port } diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index c7a996593..61636b474 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -56,7 +56,7 @@ func TestDefaultRule(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -105,7 +105,7 @@ func TestDefaultRule(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -156,7 +156,7 @@ func TestDefaultRule(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -200,7 +200,7 @@ func TestDefaultRule(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -244,7 +244,7 @@ func TestDefaultRule(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -293,7 +293,7 @@ func TestDefaultRule(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -376,7 +376,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -444,7 +444,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -454,7 +454,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, "Test2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.2:80", @@ -520,7 +520,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -573,7 +573,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -625,7 +625,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -669,7 +669,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -726,7 +726,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -773,7 +773,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -783,7 +783,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, "Service2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -990,7 +990,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1042,7 +1042,7 @@ func Test_buildConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1127,7 +1127,7 @@ func Test_buildConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1200,7 +1200,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1292,7 +1292,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1363,7 +1363,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1450,7 +1450,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1526,7 +1526,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1592,7 +1592,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1602,7 +1602,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, "Test2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.2:80", @@ -1652,7 +1652,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1703,7 +1703,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "h2c://127.0.0.1:8080", @@ -1749,7 +1749,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -1759,7 +1759,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, "Service2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:8080", @@ -1974,7 +1974,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -2035,7 +2035,7 @@ func Test_buildConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -2278,7 +2278,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", diff --git a/pkg/provider/file/fixtures/yaml/template_file.yml b/pkg/provider/file/fixtures/yaml/template_file.yml index 66903fbc8..f427dac71 100644 --- a/pkg/provider/file/fixtures/yaml/template_file.yml +++ b/pkg/provider/file/fixtures/yaml/template_file.yml @@ -1,6 +1,6 @@ http: -{{ range $i, $e := until 20 }} routers: + {{ range $i, $e := until 20 }} router{{ $e }}: service: application-1 -{{ end }} \ No newline at end of file + {{ end }} \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/fixtures/with_two_services_weight.yml b/pkg/provider/kubernetes/crd/fixtures/with_two_services_weight.yml new file mode 100644 index 000000000..76a5c7c3e --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_two_services_weight.yml @@ -0,0 +1,22 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: whoami + port: 80 + weight: 10 + - name: whoami2 + port: 8080 + weight: 0 + diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 9e3726927..f3307ab8d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -3,7 +3,6 @@ package crd import ( "context" "crypto/sha256" - "errors" "fmt" "os" "reflect" @@ -16,7 +15,6 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/job" "github.com/containous/traefik/v2/pkg/log" - "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/containous/traefik/v2/pkg/safe" "github.com/containous/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" @@ -136,159 +134,22 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } -func checkStringQuoteValidity(value string) error { - _, err := strconv.Unquote(`"` + value + `"`) - return err -} - -func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) { - service, exists, err := client.GetService(namespace, svc.Name) - if err != nil { - return nil, err +func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration { + tlsConfigs := make(map[string]*tls.CertAndStores) + conf := &dynamic.Configuration{ + HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs), + TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs), + TLS: &dynamic.TLSConfiguration{ + Certificates: getTLSConfig(tlsConfigs), + Options: buildTLSOptions(ctx, client), + }, } - if !exists { - return nil, errors.New("service not found") + for _, middleware := range client.GetMiddlewares() { + conf.HTTP.Middlewares[makeID(middleware.Namespace, middleware.Name)] = &middleware.Spec } - var portSpec *corev1.ServicePort - for _, p := range service.Spec.Ports { - if svc.Port == p.Port { - portSpec = &p - break - } - } - - if portSpec == nil { - return nil, errors.New("service port not found") - } - - var servers []dynamic.TCPServer - if service.Spec.Type == corev1.ServiceTypeExternalName { - servers = append(servers, dynamic.TCPServer{ - Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port), - }) - } else { - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) - if endpointsErr != nil { - return nil, endpointsErr - } - - if !endpointsExists { - return nil, errors.New("endpoints not found") - } - - if len(endpoints.Subsets) == 0 { - return nil, errors.New("subset not found") - } - - var port int32 - for _, subset := range endpoints.Subsets { - for _, p := range subset.Ports { - if portSpec.Name == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, errors.New("cannot define a port") - } - - for _, addr := range subset.Addresses { - servers = append(servers, dynamic.TCPServer{ - Address: fmt.Sprintf("%s:%d", addr.IP, port), - }) - } - } - } - - return servers, nil -} - -func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]dynamic.Server, error) { - strategy := svc.Strategy - if strategy == "" { - strategy = "RoundRobin" - } - if strategy != "RoundRobin" { - return nil, fmt.Errorf("load balancing strategy %v is not supported", strategy) - } - - service, exists, err := client.GetService(namespace, svc.Name) - if err != nil { - return nil, err - } - - if !exists { - return nil, errors.New("service not found") - } - - var portSpec *corev1.ServicePort - for _, p := range service.Spec.Ports { - if svc.Port == p.Port { - portSpec = &p - break - } - } - - if portSpec == nil { - return nil, errors.New("service port not found") - } - - var servers []dynamic.Server - if service.Spec.Type == corev1.ServiceTypeExternalName { - servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("http://%s:%d", service.Spec.ExternalName, portSpec.Port), - }) - } else { - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) - if endpointsErr != nil { - return nil, endpointsErr - } - - if !endpointsExists { - return nil, errors.New("endpoints not found") - } - - if len(endpoints.Subsets) == 0 { - return nil, errors.New("subset not found") - } - - var port int32 - for _, subset := range endpoints.Subsets { - for _, p := range subset.Ports { - if portSpec.Name == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, errors.New("cannot define a port") - } - - protocol := "http" - switch svc.Scheme { - case "http", "https", "h2c": - protocol = svc.Scheme - case "": - if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { - protocol = "https" - } - default: - return nil, fmt.Errorf("invalid scheme %q specified", svc.Scheme) - } - - for _, addr := range subset.Addresses { - servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), - }) - } - } - } - - return servers, nil + return conf } func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options { @@ -338,250 +199,9 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options return tlsOptions } -func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.HTTPConfiguration { - conf := &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, - } - - for _, ingressRoute := range client.GetIngressRoutes() { - logger := log.FromContext(log.With(ctx, log.Str("ingress", ingressRoute.Name), log.Str("namespace", ingressRoute.Namespace))) - - // TODO keep the name ingressClass? - if !shouldProcessIngress(p.IngressClass, ingressRoute.Annotations[annotationKubernetesIngressClass]) { - continue - } - - err := getTLSHTTP(ctx, ingressRoute, client, tlsConfigs) - if err != nil { - logger.Errorf("Error configuring TLS: %v", err) - } - - ingressName := ingressRoute.Name - if len(ingressName) == 0 { - ingressName = ingressRoute.GenerateName - } - - for _, route := range ingressRoute.Spec.Routes { - if route.Kind != "Rule" { - logger.Errorf("Unsupported match kind: %s. Only \"Rule\" is supported for now.", route.Kind) - continue - } - - if len(route.Match) == 0 { - logger.Errorf("Empty match rule") - continue - } - - if err := checkStringQuoteValidity(route.Match); err != nil { - logger.Errorf("Invalid syntax for match rule: %s", route.Match) - continue - } - - var allServers []dynamic.Server - for _, service := range route.Services { - servers, err := loadServers(client, ingressRoute.Namespace, service) - if err != nil { - logger. - WithField("serviceName", service.Name). - WithField("servicePort", service.Port). - Errorf("Cannot create service: %v", err) - continue - } - - allServers = append(allServers, servers...) - } - - var mds []string - for _, mi := range route.Middlewares { - if strings.Contains(mi.Name, "@") { - if len(mi.Namespace) > 0 { - logger. - WithField(log.MiddlewareName, mi.Name). - Warnf("namespace %q is ignored in cross-provider context", mi.Namespace) - } - mds = append(mds, mi.Name) - continue - } - - ns := mi.Namespace - if len(ns) == 0 { - ns = ingressRoute.Namespace - } - mds = append(mds, makeID(ns, mi.Name)) - } - - key, err := makeServiceKey(route.Match, ingressName) - if err != nil { - logger.Error(err) - continue - } - - serviceName := makeID(ingressRoute.Namespace, key) - - conf.Routers[serviceName] = &dynamic.Router{ - Middlewares: mds, - Priority: route.Priority, - EntryPoints: ingressRoute.Spec.EntryPoints, - Rule: route.Match, - Service: serviceName, - } - - if ingressRoute.Spec.TLS != nil { - tlsConf := &dynamic.RouterTLSConfig{ - CertResolver: ingressRoute.Spec.TLS.CertResolver, - } - - if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 { - tlsOptionsName := ingressRoute.Spec.TLS.Options.Name - // Is a Kubernetes CRD reference, (i.e. not a cross-provider reference) - ns := ingressRoute.Spec.TLS.Options.Namespace - if !strings.Contains(tlsOptionsName, "@") { - if len(ns) == 0 { - ns = ingressRoute.Namespace - } - tlsOptionsName = makeID(ns, tlsOptionsName) - } else if len(ns) > 0 { - logger. - WithField("TLSoptions", ingressRoute.Spec.TLS.Options.Name). - Warnf("namespace %q is ignored in cross-provider context", ns) - } - - tlsConf.Options = tlsOptionsName - } - conf.Routers[serviceName].TLS = tlsConf - } - - conf.Services[serviceName] = &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ - Servers: allServers, - // TODO: support other strategies. - PassHostHeader: true, - }, - } - } - } - - return conf -} - -func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.TCPConfiguration { - conf := &dynamic.TCPConfiguration{ - Routers: map[string]*dynamic.TCPRouter{}, - Services: map[string]*dynamic.TCPService{}, - } - - for _, ingressRouteTCP := range client.GetIngressRouteTCPs() { - logger := log.FromContext(log.With(ctx, log.Str("ingress", ingressRouteTCP.Name), log.Str("namespace", ingressRouteTCP.Namespace))) - - if !shouldProcessIngress(p.IngressClass, ingressRouteTCP.Annotations[annotationKubernetesIngressClass]) { - continue - } - - if ingressRouteTCP.Spec.TLS != nil && !ingressRouteTCP.Spec.TLS.Passthrough { - err := getTLSTCP(ctx, ingressRouteTCP, client, tlsConfigs) - if err != nil { - logger.Errorf("Error configuring TLS: %v", err) - } - } - - ingressName := ingressRouteTCP.Name - if len(ingressName) == 0 { - ingressName = ingressRouteTCP.GenerateName - } - - for _, route := range ingressRouteTCP.Spec.Routes { - if len(route.Match) == 0 { - logger.Errorf("Empty match rule") - continue - } - - if err := checkStringQuoteValidity(route.Match); err != nil { - logger.Errorf("Invalid syntax for match rule: %s", route.Match) - continue - } - - var allServers []dynamic.TCPServer - for _, service := range route.Services { - servers, err := loadTCPServers(client, ingressRouteTCP.Namespace, service) - if err != nil { - logger. - WithField("serviceName", service.Name). - WithField("servicePort", service.Port). - Errorf("Cannot create service: %v", err) - continue - } - - allServers = append(allServers, servers...) - } - - key, e := makeServiceKey(route.Match, ingressName) - if e != nil { - logger.Error(e) - continue - } - - serviceName := makeID(ingressRouteTCP.Namespace, key) - conf.Routers[serviceName] = &dynamic.TCPRouter{ - EntryPoints: ingressRouteTCP.Spec.EntryPoints, - Rule: route.Match, - Service: serviceName, - } - - if ingressRouteTCP.Spec.TLS != nil { - conf.Routers[serviceName].TLS = &dynamic.RouterTCPTLSConfig{ - Passthrough: ingressRouteTCP.Spec.TLS.Passthrough, - CertResolver: ingressRouteTCP.Spec.TLS.CertResolver, - } - - if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 { - tlsOptionsName := ingressRouteTCP.Spec.TLS.Options.Name - // Is a Kubernetes CRD reference (i.e. not a cross-provider reference) - ns := ingressRouteTCP.Spec.TLS.Options.Namespace - if !strings.Contains(tlsOptionsName, "@") { - if len(ns) == 0 { - ns = ingressRouteTCP.Namespace - } - tlsOptionsName = makeID(ns, tlsOptionsName) - } else if len(ns) > 0 { - logger. - WithField("TLSoptions", ingressRouteTCP.Spec.TLS.Options.Name). - Warnf("namespace %q is ignored in cross-provider context", ns) - } - - conf.Routers[serviceName].TLS.Options = tlsOptionsName - - } - } - - conf.Services[serviceName] = &dynamic.TCPService{ - LoadBalancer: &dynamic.TCPLoadBalancerService{ - Servers: allServers, - }, - } - } - } - - return conf -} - -func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration { - tlsConfigs := make(map[string]*tls.CertAndStores) - conf := &dynamic.Configuration{ - HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs), - TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs), - TLS: &dynamic.TLSConfiguration{ - Certificates: getTLSConfig(tlsConfigs), - Options: buildTLSOptions(ctx, client), - }, - } - - for _, middleware := range client.GetMiddlewares() { - conf.HTTP.Middlewares[makeID(middleware.Namespace, middleware.Name)] = &middleware.Spec - } - - return conf +func checkStringQuoteValidity(value string) error { + _, err := strconv.Unquote(`"` + value + `"`) + return err } func makeServiceKey(rule, ingressName string) (string, error) { @@ -608,50 +228,6 @@ func shouldProcessIngress(ingressClass string, ingressClassAnnotation string) bo (len(ingressClass) == 0 && ingressClassAnnotation == traefikDefaultIngressClass) } -func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { - if ingressRoute.Spec.TLS == nil { - return nil - } - if ingressRoute.Spec.TLS.SecretName == "" { - log.FromContext(ctx).Debugf("Skipping TLS sub-section: No secret name provided") - return nil - } - - configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName - if _, tlsExists := tlsConfigs[configKey]; !tlsExists { - tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace) - if err != nil { - return err - } - - tlsConfigs[configKey] = tlsConf - } - - return nil -} - -func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { - if ingressRoute.Spec.TLS == nil { - return nil - } - if ingressRoute.Spec.TLS.SecretName == "" { - log.FromContext(ctx).Debugf("Skipping TLS sub-section for TCP: No secret name provided") - return nil - } - - configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName - if _, tlsExists := tlsConfigs[configKey]; !tlsExists { - tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace) - if err != nil { - return err - } - - tlsConfigs[configKey] = tlsConf - } - - return nil -} - func getTLS(k8sClient Client, secretName, namespace string) (*tls.CertAndStores, error) { secret, exists, err := k8sClient.GetSecret(namespace, secretName) if err != nil { diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go new file mode 100644 index 000000000..52d18dd30 --- /dev/null +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -0,0 +1,274 @@ +package crd + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + "github.com/containous/traefik/v2/pkg/tls" + corev1 "k8s.io/api/core/v1" +) + +func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.HTTPConfiguration { + conf := &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + } + + for _, ingressRoute := range client.GetIngressRoutes() { + ctxRt := log.With(ctx, log.Str("ingress", ingressRoute.Name), log.Str("namespace", ingressRoute.Namespace)) + logger := log.FromContext(ctxRt) + + // TODO keep the name ingressClass? + if !shouldProcessIngress(p.IngressClass, ingressRoute.Annotations[annotationKubernetesIngressClass]) { + continue + } + + err := getTLSHTTP(ctx, ingressRoute, client, tlsConfigs) + if err != nil { + logger.Errorf("Error configuring TLS: %v", err) + } + + ingressName := ingressRoute.Name + if len(ingressName) == 0 { + ingressName = ingressRoute.GenerateName + } + + for _, route := range ingressRoute.Spec.Routes { + if route.Kind != "Rule" { + logger.Errorf("Unsupported match kind: %s. Only \"Rule\" is supported for now.", route.Kind) + continue + } + + if len(route.Match) == 0 { + logger.Errorf("Empty match rule") + continue + } + + if err := checkStringQuoteValidity(route.Match); err != nil { + logger.Errorf("Invalid syntax for match rule: %s", route.Match) + continue + } + + key, err := makeServiceKey(route.Match, ingressName) + if err != nil { + logger.Error(err) + continue + } + + serviceName := makeID(ingressRoute.Namespace, key) + + for _, service := range route.Services { + balancerServerHTTP, err := createLoadBalancerServerHTTP(client, ingressRoute, service) + if err != nil { + logger. + WithField("serviceName", service.Name). + WithField("servicePort", service.Port). + Errorf("Cannot create service: %v", err) + continue + } + + if len(route.Services) == 1 { + conf.Services[serviceName] = balancerServerHTTP + break + } + + serviceKey := fmt.Sprintf("%s-%s-%d", serviceName, service.Name, service.Port) + conf.Services[serviceKey] = balancerServerHTTP + + srv := dynamic.WRRService{Name: serviceKey} + srv.SetDefaults() + if service.Weight != nil { + srv.Weight = service.Weight + } + + if conf.Services[serviceName] == nil { + conf.Services[serviceName] = &dynamic.Service{Weighted: &dynamic.WeightedRoundRobin{}} + } + conf.Services[serviceName].Weighted.Services = append(conf.Services[serviceName].Weighted.Services, srv) + } + + var mds []string + for _, mi := range route.Middlewares { + if strings.Contains(mi.Name, "@") { + if len(mi.Namespace) > 0 { + logger. + WithField(log.MiddlewareName, mi.Name). + Warnf("namespace %q is ignored in cross-provider context", mi.Namespace) + } + mds = append(mds, mi.Name) + continue + } + + ns := mi.Namespace + if len(ns) == 0 { + ns = ingressRoute.Namespace + } + mds = append(mds, makeID(ns, mi.Name)) + } + + conf.Routers[serviceName] = &dynamic.Router{ + Middlewares: mds, + Priority: route.Priority, + EntryPoints: ingressRoute.Spec.EntryPoints, + Rule: route.Match, + Service: serviceName, + } + + if ingressRoute.Spec.TLS != nil { + tlsConf := &dynamic.RouterTLSConfig{ + CertResolver: ingressRoute.Spec.TLS.CertResolver, + } + + if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 { + tlsOptionsName := ingressRoute.Spec.TLS.Options.Name + // Is a Kubernetes CRD reference, (i.e. not a cross-provider reference) + ns := ingressRoute.Spec.TLS.Options.Namespace + if !strings.Contains(tlsOptionsName, "@") { + if len(ns) == 0 { + ns = ingressRoute.Namespace + } + tlsOptionsName = makeID(ns, tlsOptionsName) + } else if len(ns) > 0 { + logger. + WithField("TLSoptions", ingressRoute.Spec.TLS.Options.Name). + Warnf("namespace %q is ignored in cross-provider context", ns) + } + + tlsConf.Options = tlsOptionsName + } + conf.Routers[serviceName].TLS = tlsConf + } + + } + } + + return conf +} + +func createLoadBalancerServerHTTP(client Client, ingressRoute *v1alpha1.IngressRoute, service v1alpha1.Service) (*dynamic.Service, error) { + servers, err := loadServers(client, ingressRoute.Namespace, service) + if err != nil { + return nil, err + } + + return &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: servers, + // TODO: support other strategies. + PassHostHeader: true, + }, + }, nil +} + +func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]dynamic.Server, error) { + strategy := svc.Strategy + if strategy == "" { + strategy = "RoundRobin" + } + if strategy != "RoundRobin" { + return nil, fmt.Errorf("load balancing strategy %v is not supported", strategy) + } + + service, exists, err := client.GetService(namespace, svc.Name) + if err != nil { + return nil, err + } + + if !exists { + return nil, errors.New("service not found") + } + + var portSpec *corev1.ServicePort + for _, p := range service.Spec.Ports { + if svc.Port == p.Port { + portSpec = &p + break + } + } + + if portSpec == nil { + return nil, errors.New("service port not found") + } + + var servers []dynamic.Server + if service.Spec.Type == corev1.ServiceTypeExternalName { + servers = append(servers, dynamic.Server{ + URL: fmt.Sprintf("http://%s:%d", service.Spec.ExternalName, portSpec.Port), + }) + } else { + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) + if endpointsErr != nil { + return nil, endpointsErr + } + + if !endpointsExists { + return nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, errors.New("subset not found") + } + + var port int32 + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, errors.New("cannot define a port") + } + + protocol := "http" + switch svc.Scheme { + case "http", "https", "h2c": + protocol = svc.Scheme + case "": + if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { + protocol = "https" + } + default: + return nil, fmt.Errorf("invalid scheme %q specified", svc.Scheme) + } + + for _, addr := range subset.Addresses { + servers = append(servers, dynamic.Server{ + URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), + }) + } + } + } + + return servers, nil +} + +func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { + if ingressRoute.Spec.TLS == nil { + return nil + } + if ingressRoute.Spec.TLS.SecretName == "" { + log.FromContext(ctx).Debugf("No secret name provided") + return nil + } + + configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName + if _, tlsExists := tlsConfigs[configKey]; !tlsExists { + tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace) + if err != nil { + return err + } + + tlsConfigs[configKey] = tlsConf + } + + return nil +} diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go new file mode 100644 index 000000000..ea64d89ca --- /dev/null +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -0,0 +1,201 @@ +package crd + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + "github.com/containous/traefik/v2/pkg/tls" + corev1 "k8s.io/api/core/v1" +) + +func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.TCPConfiguration { + conf := &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + } + + for _, ingressRouteTCP := range client.GetIngressRouteTCPs() { + logger := log.FromContext(log.With(ctx, log.Str("ingress", ingressRouteTCP.Name), log.Str("namespace", ingressRouteTCP.Namespace))) + + if !shouldProcessIngress(p.IngressClass, ingressRouteTCP.Annotations[annotationKubernetesIngressClass]) { + continue + } + + if ingressRouteTCP.Spec.TLS != nil && !ingressRouteTCP.Spec.TLS.Passthrough { + err := getTLSTCP(ctx, ingressRouteTCP, client, tlsConfigs) + if err != nil { + logger.Errorf("Error configuring TLS: %v", err) + } + } + + ingressName := ingressRouteTCP.Name + if len(ingressName) == 0 { + ingressName = ingressRouteTCP.GenerateName + } + + for _, route := range ingressRouteTCP.Spec.Routes { + if len(route.Match) == 0 { + logger.Errorf("Empty match rule") + continue + } + + if err := checkStringQuoteValidity(route.Match); err != nil { + logger.Errorf("Invalid syntax for match rule: %s", route.Match) + continue + } + + var allServers []dynamic.TCPServer + for _, service := range route.Services { + servers, err := loadTCPServers(client, ingressRouteTCP.Namespace, service) + if err != nil { + logger. + WithField("serviceName", service.Name). + WithField("servicePort", service.Port). + Errorf("Cannot create service: %v", err) + continue + } + + allServers = append(allServers, servers...) + } + + key, e := makeServiceKey(route.Match, ingressName) + if e != nil { + logger.Error(e) + continue + } + + serviceName := makeID(ingressRouteTCP.Namespace, key) + conf.Routers[serviceName] = &dynamic.TCPRouter{ + EntryPoints: ingressRouteTCP.Spec.EntryPoints, + Rule: route.Match, + Service: serviceName, + } + + if ingressRouteTCP.Spec.TLS != nil { + conf.Routers[serviceName].TLS = &dynamic.RouterTCPTLSConfig{ + Passthrough: ingressRouteTCP.Spec.TLS.Passthrough, + CertResolver: ingressRouteTCP.Spec.TLS.CertResolver, + } + + if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 { + tlsOptionsName := ingressRouteTCP.Spec.TLS.Options.Name + // Is a Kubernetes CRD reference (i.e. not a cross-provider reference) + ns := ingressRouteTCP.Spec.TLS.Options.Namespace + if !strings.Contains(tlsOptionsName, "@") { + if len(ns) == 0 { + ns = ingressRouteTCP.Namespace + } + tlsOptionsName = makeID(ns, tlsOptionsName) + } else if len(ns) > 0 { + logger. + WithField("TLSoptions", ingressRouteTCP.Spec.TLS.Options.Name). + Warnf("namespace %q is ignored in cross-provider context", ns) + } + + conf.Routers[serviceName].TLS.Options = tlsOptionsName + + } + } + + conf.Services[serviceName] = &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPLoadBalancerService{ + Servers: allServers, + }, + } + } + } + + return conf +} + +func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) { + service, exists, err := client.GetService(namespace, svc.Name) + if err != nil { + return nil, err + } + + if !exists { + return nil, errors.New("service not found") + } + + var portSpec *corev1.ServicePort + for _, p := range service.Spec.Ports { + if svc.Port == p.Port { + portSpec = &p + break + } + } + + if portSpec == nil { + return nil, errors.New("service port not found") + } + + var servers []dynamic.TCPServer + if service.Spec.Type == corev1.ServiceTypeExternalName { + servers = append(servers, dynamic.TCPServer{ + Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port), + }) + } else { + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) + if endpointsErr != nil { + return nil, endpointsErr + } + + if !endpointsExists { + return nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, errors.New("subset not found") + } + + var port int32 + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, errors.New("cannot define a port") + } + + for _, addr := range subset.Addresses { + servers = append(servers, dynamic.TCPServer{ + Address: fmt.Sprintf("%s:%d", addr.IP, port), + }) + } + } + } + + return servers, nil +} + +func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { + if ingressRoute.Spec.TLS == nil { + return nil + } + if ingressRoute.Spec.TLS.SecretName == "" { + log.FromContext(ctx).Debugf("No secret name provided") + return nil + } + + configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName + if _, tlsExists := tlsConfigs[configKey]; !tlsExists { + tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace) + if err != nil { + return err + } + + tlsConfigs[configKey] = tlsConf + } + + return nil +} diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 75e7f122b..cb9d87408 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -12,6 +12,8 @@ import ( var _ provider.Provider = (*Provider)(nil) +func Int(v int) *int { return &v } + func TestLoadIngressRouteTCPs(t *testing.T) { testCases := []struct { desc string @@ -671,7 +673,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -720,7 +722,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Services: map[string]*dynamic.Service{ "default/test2.route-23c7f4c450289ee29016": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -770,7 +772,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Services: map[string]*dynamic.Service{ "default/test2.route-23c7f4c450289ee29016": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -812,7 +814,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -825,7 +827,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, "default/test.route-77c62dfe9517144aeeaa": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -843,7 +845,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, { - desc: "One ingress Route with two different services, their servers will merge", + desc: "One ingress Route with two different services", paths: []string{"services.yml", "with_two_services.yml"}, expected: &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{}, @@ -863,7 +865,21 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-77c62dfe9517144aeeaa": { - LoadBalancer: &dynamic.LoadBalancerService{ + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default/test.route-77c62dfe9517144aeeaa-whoami-80", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default/test.route-77c62dfe9517144aeeaa-whoami2-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default/test.route-77c62dfe9517144aeeaa-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -871,6 +887,77 @@ func TestLoadIngressRoutes(t *testing.T) { { URL: "http://10.10.0.2:80", }, + }, + PassHostHeader: true, + }, + }, + "default/test.route-77c62dfe9517144aeeaa-whoami2-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: true, + }, + }, + }, + }, + }, + }, + { + desc: "One ingress Route with two different services, with weights", + paths: []string{"services.yml", "with_two_services_weight.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default/test.route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default/test.route-77c62dfe9517144aeeaa", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default/test.route-77c62dfe9517144aeeaa": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default/test.route-77c62dfe9517144aeeaa-whoami-80", + Weight: Int(10), + }, + { + Name: "default/test.route-77c62dfe9517144aeeaa-whoami2-8080", + Weight: Int(0), + }, + }, + }, + }, + "default/test.route-77c62dfe9517144aeeaa-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: true, + }, + }, + "default/test.route-77c62dfe9517144aeeaa-whoami2-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ { URL: "http://10.10.0.3:8080", }, @@ -981,7 +1068,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1039,7 +1126,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1097,7 +1184,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1154,7 +1241,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1200,7 +1287,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1246,7 +1333,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1284,7 +1371,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://10.10.0.1:80", @@ -1321,7 +1408,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "https://10.10.0.5:8443", @@ -1358,7 +1445,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "default/test.route-6b204d94623b3df4370c": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "https://10.10.0.7:8443", diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go index ac7eb32e3..bf6074039 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go @@ -49,6 +49,7 @@ type Service struct { Scheme string `json:"scheme,omitempty"` HealthCheck *HealthCheck `json:"healthCheck,omitempty"` Strategy string `json:"strategy,omitempty"` + Weight *int `json:"weight,omitempty"` } // MiddlewareRef is a ref to the Middleware resources. diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 502a1d854..047f0fcaa 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -395,6 +395,11 @@ func (in *Service) DeepCopyInto(out *Service) { *out = new(HealthCheck) (*in).DeepCopyInto(*out) } + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } return } diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index c4874547e..8f17bf174 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -229,7 +229,7 @@ func loadService(client Client, namespace string, backend v1beta1.IngressBackend } return &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: servers, PassHostHeader: true, }, diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index d5ef16a2e..bc20a25b1 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -50,7 +50,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -84,7 +84,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -118,7 +118,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -148,7 +148,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -177,7 +177,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/example-com/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -208,7 +208,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -242,7 +242,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -276,7 +276,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -317,7 +317,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -362,7 +362,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -375,7 +375,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, "testing/service2/8082": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -428,7 +428,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "default-backend": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -458,7 +458,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -488,7 +488,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/tchouk": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -518,7 +518,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/tchouk": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -552,7 +552,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/tchouk": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -565,7 +565,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, "testing/service1/carotte": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -599,7 +599,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/tchouk": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -612,7 +612,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, "toto/service1/tchouk": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -664,7 +664,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/8080": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -696,7 +696,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/example-com/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -733,7 +733,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/443": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -763,7 +763,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/8443": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -794,7 +794,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/8443": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -825,7 +825,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "default-backend": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { @@ -855,7 +855,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, Services: map[string]*dynamic.Service{ "testing/service1/80": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: true, Servers: []dynamic.Server{ { diff --git a/pkg/provider/marathon/config.go b/pkg/provider/marathon/config.go index cd1d81939..3f9adfebe 100644 --- a/pkg/provider/marathon/config.go +++ b/pkg/provider/marathon/config.go @@ -98,7 +98,7 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, app marathon.A if len(conf.Services) == 0 { conf.Services = make(map[string]*dynamic.Service) - lb := &dynamic.LoadBalancerService{} + lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() conf.Services[appName] = &dynamic.Service{ LoadBalancer: lb, diff --git a/pkg/provider/marathon/config_test.go b/pkg/provider/marathon/config_test.go index 8d9504e35..8bc35e958 100644 --- a/pkg/provider/marathon/config_test.go +++ b/pkg/provider/marathon/config_test.go @@ -56,7 +56,7 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "app": {LoadBalancer: &dynamic.LoadBalancerService{ + "app": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -110,7 +110,7 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "app": {LoadBalancer: &dynamic.LoadBalancerService{ + "app": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -156,7 +156,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, Services: map[string]*dynamic.Service{ - "app": {LoadBalancer: &dynamic.LoadBalancerService{ + "app": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -200,7 +200,7 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "Service1": {LoadBalancer: &dynamic.LoadBalancerService{ + "Service1": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:8080", @@ -249,7 +249,7 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "Service1": {LoadBalancer: &dynamic.LoadBalancerService{ + "Service1": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:8080", @@ -300,7 +300,7 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "foo": {LoadBalancer: &dynamic.LoadBalancerService{ + "foo": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:8080", @@ -308,7 +308,7 @@ func TestBuildConfiguration(t *testing.T) { }, PassHostHeader: true, }}, - "bar": {LoadBalancer: &dynamic.LoadBalancerService{ + "bar": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:8081", @@ -343,7 +343,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -382,7 +382,7 @@ func TestBuildConfiguration(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "Service1": {LoadBalancer: &dynamic.LoadBalancerService{ + "Service1": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -420,7 +420,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -451,7 +451,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -495,7 +495,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -529,7 +529,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -539,7 +539,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, "Service2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -629,7 +629,7 @@ func TestBuildConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -639,7 +639,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, "app2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -686,7 +686,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -696,7 +696,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, "app2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -734,7 +734,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -744,7 +744,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, "app2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -789,7 +789,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -830,7 +830,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -840,7 +840,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, "app2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -877,7 +877,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -915,7 +915,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "h2c://localhost:90", @@ -948,7 +948,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -958,7 +958,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, "Service2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:8080", @@ -1123,7 +1123,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -1161,7 +1161,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -1198,7 +1198,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "a_b_app": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", @@ -1362,7 +1362,7 @@ func TestBuildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "bar": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://localhost:80", diff --git a/pkg/provider/rancher/config.go b/pkg/provider/rancher/config.go index 24a32e66c..4d6f63a13 100644 --- a/pkg/provider/rancher/config.go +++ b/pkg/provider/rancher/config.go @@ -96,7 +96,7 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, service ranche if len(configuration.Services) == 0 { configuration.Services = make(map[string]*dynamic.Service) - lb := &dynamic.LoadBalancerService{} + lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() configuration.Services[serviceName] = &dynamic.Service{ LoadBalancer: lb, @@ -183,7 +183,7 @@ func (p *Provider) addServerTCP(ctx context.Context, service rancherData, loadBa } -func (p *Provider) addServers(ctx context.Context, service rancherData, loadBalancer *dynamic.LoadBalancerService) error { +func (p *Provider) addServers(ctx context.Context, service rancherData, loadBalancer *dynamic.ServersLoadBalancer) error { log.FromContext(ctx).Debugf("Trying to add servers for service %s \n", service.Name) serverPort := getLBServerPort(loadBalancer) @@ -216,7 +216,7 @@ func (p *Provider) addServers(ctx context.Context, service rancherData, loadBala return nil } -func getLBServerPort(loadBalancer *dynamic.LoadBalancerService) string { +func getLBServerPort(loadBalancer *dynamic.ServersLoadBalancer) string { if loadBalancer != nil && len(loadBalancer.Servers) > 0 { return loadBalancer.Servers[0].Port } diff --git a/pkg/provider/rancher/config_test.go b/pkg/provider/rancher/config_test.go index cc40a74c6..31fb29ddc 100644 --- a/pkg/provider/rancher/config_test.go +++ b/pkg/provider/rancher/config_test.go @@ -43,7 +43,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -95,7 +95,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -105,7 +105,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, "Test2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.2:80", @@ -157,7 +157,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -170,7 +170,7 @@ func Test_buildConfiguration(t *testing.T) { }, }, "Test2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://128.0.0.1:80", @@ -214,7 +214,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -302,7 +302,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -372,7 +372,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -425,7 +425,7 @@ func Test_buildConfiguration(t *testing.T) { }, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -467,7 +467,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Test": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", @@ -652,7 +652,7 @@ func Test_buildConfiguration(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ "Service1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:80", diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 02b8606e5..66b11487d 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -48,7 +48,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -85,7 +85,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -108,7 +108,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -132,7 +132,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -173,7 +173,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -213,7 +213,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service@provider-1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -236,7 +236,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service@provider-2": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -260,7 +260,7 @@ func TestRouterManager_Get(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service@provider-1": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -355,7 +355,7 @@ func TestAccessLog(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -383,7 +383,7 @@ func TestAccessLog(t *testing.T) { }, serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -448,7 +448,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "No error", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1:8085", @@ -482,7 +482,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "One router with wrong rule", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -509,7 +509,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "All router with wrong rule", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -536,7 +536,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "Router with unknown service", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -579,7 +579,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "Router with middleware", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -619,7 +619,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "Router with unknown middleware", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -650,7 +650,7 @@ func TestRuntimeConfiguration(t *testing.T) { desc: "Router with broken middleware", serviceConfig: map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://127.0.0.1", @@ -749,7 +749,7 @@ func BenchmarkRouterServe(b *testing.B) { } serviceConfig := map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server.URL, @@ -793,7 +793,7 @@ func BenchmarkService(b *testing.B) { serviceConfig := map[string]*dynamic.Service{ "foo-service": { - LoadBalancer: &dynamic.LoadBalancerService{ + LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "tchouck", diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 9f0913742..868e282be 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -199,7 +199,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithRule(routeRule)), ), th.WithLoadBalancerServices(th.WithService("bar", - th.WithStickiness("test")), + th.WithSticky("test")), ), ) }, @@ -229,7 +229,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithRule(routeRule)), ), th.WithLoadBalancerServices(th.WithService("bar", - th.WithStickiness("test")), + th.WithSticky("test")), ), ) }, diff --git a/pkg/server/service/loadbalancer/wrr/wrr.go b/pkg/server/service/loadbalancer/wrr/wrr.go new file mode 100644 index 000000000..a805c66a9 --- /dev/null +++ b/pkg/server/service/loadbalancer/wrr/wrr.go @@ -0,0 +1,155 @@ +package wrr + +import ( + "fmt" + "net/http" + "sync" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/log" +) + +type namedHandler struct { + http.Handler + name string + weight int +} + +type stickyCookie struct { + name string + secure bool + httpOnly bool +} + +// New creates a new load balancer. +func New(sticky *dynamic.Sticky) *Balancer { + balancer := &Balancer{ + mutex: &sync.Mutex{}, + index: -1, + } + if sticky != nil && sticky.Cookie != nil { + balancer.stickyCookie = &stickyCookie{ + name: sticky.Cookie.Name, + secure: sticky.Cookie.Secure, + httpOnly: sticky.Cookie.HTTPOnly, + } + } + return balancer +} + +// Balancer is a WeightedRoundRobin load balancer. +type Balancer struct { + handlers []*namedHandler + mutex *sync.Mutex + // Current index (starts from -1) + index int + currentWeight int + stickyCookie *stickyCookie +} + +func (b *Balancer) maxWeight() int { + max := -1 + for _, s := range b.handlers { + if s.weight > max { + max = s.weight + } + } + return max +} + +func (b *Balancer) weightGcd() int { + divisor := -1 + for _, s := range b.handlers { + if divisor == -1 { + divisor = s.weight + } else { + divisor = gcd(divisor, s.weight) + } + } + return divisor +} + +func gcd(a, b int) int { + for b != 0 { + a, b = b, a%b + } + return a +} + +func (b *Balancer) nextServer() (*namedHandler, error) { + b.mutex.Lock() + defer b.mutex.Unlock() + + if len(b.handlers) == 0 { + return nil, fmt.Errorf("no servers in the pool") + } + + // The algo below may look messy, but is actually very simple + // it calculates the GCD and subtracts it on every iteration, what interleaves servers + // and allows us not to build an iterator every time we readjust weights + + // GCD across all enabled servers + gcd := b.weightGcd() + // Maximum weight across all enabled servers + max := b.maxWeight() + + for { + b.index = (b.index + 1) % len(b.handlers) + if b.index == 0 { + b.currentWeight -= gcd + if b.currentWeight <= 0 { + b.currentWeight = max + if b.currentWeight == 0 { + return nil, fmt.Errorf("all servers have 0 weight") + } + } + } + srv := b.handlers[b.index] + if srv.weight >= b.currentWeight { + log.WithoutContext().Debugf("Service Select: %s", srv.name) + return srv, nil + } + } +} + +func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if b.stickyCookie != nil { + cookie, err := req.Cookie(b.stickyCookie.name) + + if err != nil && err != http.ErrNoCookie { + log.WithoutContext().Warnf("Error while reading cookie: %v", err) + } + + if err == nil && cookie != nil { + for _, handler := range b.handlers { + if handler.name == cookie.Value { + handler.ServeHTTP(w, req) + return + } + } + } + } + + server, err := b.nextServer() + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError)+err.Error(), http.StatusInternalServerError) + return + } + + if b.stickyCookie != nil { + cookie := &http.Cookie{Name: b.stickyCookie.name, Value: server.name, Path: "/", HttpOnly: b.stickyCookie.httpOnly, Secure: b.stickyCookie.secure} + http.SetCookie(w, cookie) + } + + server.ServeHTTP(w, req) +} + +// AddService adds a handler. +// It is not thread safe with ServeHTTP. +func (b *Balancer) AddService(name string, handler http.Handler, weight *int) { + w := 1 + if weight != nil { + w = *weight + } + b.handlers = append(b.handlers, &namedHandler{Handler: handler, name: name, weight: w}) +} diff --git a/pkg/server/service/loadbalancer/wrr/wrr_test.go b/pkg/server/service/loadbalancer/wrr/wrr_test.go new file mode 100644 index 000000000..b2eb65e5a --- /dev/null +++ b/pkg/server/service/loadbalancer/wrr/wrr_test.go @@ -0,0 +1,115 @@ +package wrr + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/stretchr/testify/assert" +) + +func Int(v int) *int { return &v } + +type responseRecorder struct { + *httptest.ResponseRecorder + save map[string]int +} + +func (r *responseRecorder) WriteHeader(statusCode int) { + r.save[r.Header().Get("server")]++ + r.ResponseRecorder.WriteHeader(statusCode) + +} + +func TestBalancer(t *testing.T) { + balancer := New(nil) + + balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("server", "first") + rw.WriteHeader(http.StatusOK) + }), Int(3)) + + balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("server", "second") + rw.WriteHeader(http.StatusOK) + }), Int(1)) + + recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}} + for i := 0; i < 4; i++ { + balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) + } + + assert.Equal(t, 3, recorder.save["first"]) + assert.Equal(t, 1, recorder.save["second"]) +} + +func TestBalancerNoService(t *testing.T) { + balancer := New(nil) + + recorder := httptest.NewRecorder() + balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) + + assert.Equal(t, http.StatusInternalServerError, recorder.Result().StatusCode) +} + +func TestBalancerOneServerZeroWeight(t *testing.T) { + balancer := New(nil) + + balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("server", "first") + rw.WriteHeader(http.StatusOK) + }), Int(1)) + + balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) + + recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}} + for i := 0; i < 3; i++ { + balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) + } + + assert.Equal(t, 3, recorder.save["first"]) +} + +func TestBalancerAllServersZeroWeight(t *testing.T) { + balancer := New(nil) + + balancer.AddService("test", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) + balancer.AddService("test2", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) + + recorder := httptest.NewRecorder() + balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) + + assert.Equal(t, http.StatusInternalServerError, recorder.Result().StatusCode) +} + +func TestSticky(t *testing.T) { + balancer := New(&dynamic.Sticky{ + Cookie: &dynamic.Cookie{Name: "test"}, + }) + + balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("server", "first") + rw.WriteHeader(http.StatusOK) + }), Int(1)) + + balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("server", "second") + rw.WriteHeader(http.StatusOK) + }), Int(2)) + + recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}} + + req := httptest.NewRequest(http.MethodGet, "/", nil) + for i := 0; i < 3; i++ { + for _, cookie := range recorder.Result().Cookies() { + req.AddCookie(cookie) + } + recorder.ResponseRecorder = httptest.NewRecorder() + + balancer.ServeHTTP(recorder, req) + } + + assert.Equal(t, 0, recorder.save["first"]) + assert.Equal(t, 3, recorder.save["second"]) +} diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 7e2a0e8f1..1ea363817 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "net/http" "net/http/httputil" @@ -20,6 +21,7 @@ import ( "github.com/containous/traefik/v2/pkg/middlewares/pipelining" "github.com/containous/traefik/v2/pkg/server/cookie" "github.com/containous/traefik/v2/pkg/server/internal" + "github.com/containous/traefik/v2/pkg/server/service/loadbalancer/wrr" "github.com/vulcand/oxy/roundrobin" ) @@ -60,27 +62,58 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, respons return nil, fmt.Errorf("the service %q does not exist", serviceName) } - // TODO Should handle multiple service types - // FIXME Check if the service is declared multiple times with different types - if conf.LoadBalancer == nil { - sErr := fmt.Errorf("the service %q doesn't have any load balancer", serviceName) - conf.AddError(sErr, true) - return nil, sErr + if conf.LoadBalancer != nil && conf.Weighted != nil { + return nil, errors.New("cannot create service: multi-types service not supported, consider declaring two different pieces of service instead") } - lb, err := m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier) - if err != nil { - conf.AddError(err, true) - return nil, err + var lb http.Handler + + switch { + case conf.LoadBalancer != nil: + var err error + lb, err = m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier) + if err != nil { + conf.AddError(err, true) + return nil, err + } + case conf.Weighted != nil: + var err error + lb, err = m.getLoadBalancerWRRServiceHandler(ctx, serviceName, conf.Weighted, responseModifier) + if err != nil { + conf.AddError(err, true) + return nil, err + } + default: + sErr := fmt.Errorf("the service %q does not have any type defined", serviceName) + conf.AddError(sErr, true) + return nil, sErr } return lb, nil } +func (m *Manager) getLoadBalancerWRRServiceHandler(ctx context.Context, serviceName string, config *dynamic.WeightedRoundRobin, responseModifier func(*http.Response) error) (http.Handler, error) { + // TODO Handle accesslog and metrics with multiple service name + if config.Sticky != nil && config.Sticky.Cookie != nil { + config.Sticky.Cookie.Name = cookie.GetName(config.Sticky.Cookie.Name, serviceName) + } + + balancer := wrr.New(config.Sticky) + for _, service := range config.Services { + serviceHandler, err := m.BuildHTTP(ctx, service.Name, responseModifier) + if err != nil { + return nil, err + } + + balancer.AddService(service.Name, serviceHandler, service.Weight) + } + return balancer, nil +} + func (m *Manager) getLoadBalancerServiceHandler( ctx context.Context, serviceName string, - service *dynamic.LoadBalancerService, + service *dynamic.ServersLoadBalancer, responseModifier func(*http.Response) error, ) (http.Handler, error) { fwd, err := buildProxy(service.PassHostHeader, service.ResponseForwarding, m.defaultRoundTripper, m.bufferPool, responseModifier) @@ -193,16 +226,16 @@ func buildHealthCheckOptions(ctx context.Context, lb healthcheck.BalancerHandler } } -func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, service *dynamic.LoadBalancerService, fwd http.Handler) (healthcheck.BalancerHandler, error) { +func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, service *dynamic.ServersLoadBalancer, fwd http.Handler) (healthcheck.BalancerHandler, error) { logger := log.FromContext(ctx) logger.Debug("Creating load-balancer") var options []roundrobin.LBOption var cookieName string - if stickiness := service.Stickiness; stickiness != nil { - cookieName = cookie.GetName(stickiness.CookieName, serviceName) - opts := roundrobin.CookieOptions{HTTPOnly: stickiness.HTTPOnlyCookie, Secure: stickiness.SecureCookie} + if service.Sticky != nil && service.Sticky.Cookie != nil { + cookieName = cookie.GetName(service.Sticky.Cookie.Name, serviceName) + opts := roundrobin.CookieOptions{HTTPOnly: service.Sticky.Cookie.HTTPOnly, Secure: service.Sticky.Cookie.Secure} options = append(options, roundrobin.EnableStickySession(roundrobin.NewStickySessionWithOptions(cookieName, opts))) logger.Debugf("Sticky session cookie name: %v", cookieName) } diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index a075b5cae..e150d0cc7 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -27,14 +27,14 @@ func TestGetLoadBalancer(t *testing.T) { testCases := []struct { desc string serviceName string - service *dynamic.LoadBalancerService + service *dynamic.ServersLoadBalancer fwd http.Handler expectError bool }{ { desc: "Fails when provided an invalid URL", serviceName: "test", - service: &dynamic.LoadBalancerService{ + service: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: ":", @@ -47,15 +47,15 @@ func TestGetLoadBalancer(t *testing.T) { { desc: "Succeeds when there are no servers", serviceName: "test", - service: &dynamic.LoadBalancerService{}, + service: &dynamic.ServersLoadBalancer{}, fwd: &MockForwarder{}, expectError: false, }, { - desc: "Succeeds when stickiness is set", + desc: "Succeeds when sticky.cookie is set", serviceName: "test", - service: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{}, + service: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}}, }, fwd: &MockForwarder{}, expectError: false, @@ -114,7 +114,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { testCases := []struct { desc string serviceName string - service *dynamic.LoadBalancerService + service *dynamic.ServersLoadBalancer responseModifier func(*http.Response) error expected []ExpectedResult @@ -122,7 +122,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { { desc: "Load balances between the two servers", serviceName: "test", - service: &dynamic.LoadBalancerService{ + service: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: server1.URL, @@ -146,7 +146,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { { desc: "StatusBadGateway when the server is not reachable", serviceName: "test", - service: &dynamic.LoadBalancerService{ + service: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { URL: "http://foo", @@ -162,7 +162,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { { desc: "ServiceUnavailable when no servers are available", serviceName: "test", - service: &dynamic.LoadBalancerService{ + service: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{}, }, expected: []ExpectedResult{ @@ -172,10 +172,10 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { }, }, { - desc: "Always call the same server when stickiness is true", + desc: "Always call the same server when sticky.cookie is true", serviceName: "test", - service: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{}, + service: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}}, Servers: []dynamic.Server{ { URL: server1.URL, @@ -199,8 +199,8 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { { desc: "Sticky Cookie's options set correctly", serviceName: "test", - service: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{HTTPOnlyCookie: true, SecureCookie: true}, + service: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{HTTPOnly: true, Secure: true}}, Servers: []dynamic.Server{ { URL: server1.URL, @@ -219,8 +219,8 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { { desc: "PassHost passes the host instead of the IP", serviceName: "test", - service: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{}, + service: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}}, PassHostHeader: true, Servers: []dynamic.Server{ { @@ -238,8 +238,8 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { { desc: "PassHost doesn't passe the host instead of the IP", serviceName: "test", - service: &dynamic.LoadBalancerService{ - Stickiness: &dynamic.Stickiness{}, + service: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}}, Servers: []dynamic.Server{ { URL: serverPassHostFalse.URL, @@ -297,7 +297,7 @@ func TestManager_Build(t *testing.T) { configs: map[string]*runtime.ServiceInfo{ "serviceName": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{}, + LoadBalancer: &dynamic.ServersLoadBalancer{}, }, }, }, @@ -308,7 +308,7 @@ func TestManager_Build(t *testing.T) { configs: map[string]*runtime.ServiceInfo{ "serviceName@provider-1": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{}, + LoadBalancer: &dynamic.ServersLoadBalancer{}, }, }, }, @@ -319,7 +319,7 @@ func TestManager_Build(t *testing.T) { configs: map[string]*runtime.ServiceInfo{ "serviceName@provider-1": { Service: &dynamic.Service{ - LoadBalancer: &dynamic.LoadBalancerService{}, + LoadBalancer: &dynamic.ServersLoadBalancer{}, }, }, }, diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go index e0f9b807b..5f6c48165 100644 --- a/pkg/testhelpers/config.go +++ b/pkg/testhelpers/config.go @@ -50,11 +50,11 @@ func WithServiceName(serviceName string) func(*dynamic.Router) { } // WithLoadBalancerServices is a helper to create a configuration. -func WithLoadBalancerServices(opts ...func(service *dynamic.LoadBalancerService) string) func(*dynamic.HTTPConfiguration) { +func WithLoadBalancerServices(opts ...func(service *dynamic.ServersLoadBalancer) string) func(*dynamic.HTTPConfiguration) { return func(c *dynamic.HTTPConfiguration) { c.Services = make(map[string]*dynamic.Service) for _, opt := range opts { - b := &dynamic.LoadBalancerService{} + b := &dynamic.ServersLoadBalancer{} name := opt(b) c.Services[name] = &dynamic.Service{ LoadBalancer: b, @@ -64,8 +64,8 @@ func WithLoadBalancerServices(opts ...func(service *dynamic.LoadBalancerService) } // WithService is a helper to create a configuration. -func WithService(name string, opts ...func(*dynamic.LoadBalancerService)) func(*dynamic.LoadBalancerService) string { - return func(r *dynamic.LoadBalancerService) string { +func WithService(name string, opts ...func(*dynamic.ServersLoadBalancer)) func(*dynamic.ServersLoadBalancer) string { + return func(r *dynamic.ServersLoadBalancer) string { for _, opt := range opts { opt(r) } @@ -117,8 +117,8 @@ func WithRule(rule string) func(*dynamic.Router) { } // WithServers is a helper to create a configuration. -func WithServers(opts ...func(*dynamic.Server)) func(*dynamic.LoadBalancerService) { - return func(b *dynamic.LoadBalancerService) { +func WithServers(opts ...func(*dynamic.Server)) func(*dynamic.ServersLoadBalancer) { + return func(b *dynamic.ServersLoadBalancer) { for _, opt := range opts { server := dynamic.Server{} opt(&server) @@ -137,11 +137,11 @@ func WithServer(url string, opts ...func(*dynamic.Server)) func(*dynamic.Server) } } -// WithStickiness is a helper to create a configuration. -func WithStickiness(cookieName string) func(*dynamic.LoadBalancerService) { - return func(b *dynamic.LoadBalancerService) { - b.Stickiness = &dynamic.Stickiness{ - CookieName: cookieName, +// WithSticky is a helper to create a configuration. +func WithSticky(cookieName string) func(*dynamic.ServersLoadBalancer) { + return func(b *dynamic.ServersLoadBalancer) { + b.Sticky = &dynamic.Sticky{ + Cookie: &dynamic.Cookie{Name: cookieName}, } } }