From 7e0eefe20acdd9d8167042b4f5e2529a7685f1bb Mon Sep 17 00:00:00 2001 From: Helene Durand Date: Fri, 8 Nov 2024 14:26:59 +0100 Subject: [PATCH] MEDIUM: new annotation cookie-persistence-no-dynamic This new annotation allows to set a cookie on the server line with the server name as value --- .../cookie-persistence/config/deploy.yml.tmpl | 67 ++++ .../cookie-persistence_test.go | 331 ++++++++++++++++++ .../e2e/cookie-persistence/suite_test.go | 122 +++++++ documentation/annotations.md | 31 ++ documentation/doc.yaml | 20 ++ pkg/annotations/service/cookie.go | 39 ++- pkg/service/endpoints.go | 5 +- pkg/service/service.go | 32 +- 8 files changed, 633 insertions(+), 14 deletions(-) create mode 100644 deploy/tests/e2e/cookie-persistence/config/deploy.yml.tmpl create mode 100644 deploy/tests/e2e/cookie-persistence/cookie-persistence_test.go create mode 100644 deploy/tests/e2e/cookie-persistence/suite_test.go diff --git a/deploy/tests/e2e/cookie-persistence/config/deploy.yml.tmpl b/deploy/tests/e2e/cookie-persistence/config/deploy.yml.tmpl new file mode 100644 index 00000000..87942c9b --- /dev/null +++ b/deploy/tests/e2e/cookie-persistence/config/deploy.yml.tmpl @@ -0,0 +1,67 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: http-echo +spec: + replicas: 1 + selector: + matchLabels: + app: http-echo + template: + metadata: + labels: + app: http-echo + spec: + containers: + - name: http-echo + image: "haproxytech/http-echo:latest" + imagePullPolicy: Never + ports: + - name: http + containerPort: 8888 + protocol: TCP + - name: https + containerPort: 8443 + protocol: TCP +--- +kind: Service +apiVersion: v1 +metadata: + name: http-echo +spec: + ports: + - name: http + protocol: TCP + port: 80 + targetPort: http + - name: https + protocol: TCP + port: 443 + targetPort: https + selector: + app: http-echo +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: http-echo + annotations: + ingress.class: haproxy +{{ if .CookiePersistenceDynamic }} + cookie-persistence: "mycookie" +{{ else if .CookiePersistenceNoDynamic }} + cookie-persistence-no-dynamic: "mycookie" +{{ end }} + +spec: + rules: + - host: {{ .Host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: http-echo + port: + name: http diff --git a/deploy/tests/e2e/cookie-persistence/cookie-persistence_test.go b/deploy/tests/e2e/cookie-persistence/cookie-persistence_test.go new file mode 100644 index 00000000..9513b115 --- /dev/null +++ b/deploy/tests/e2e/cookie-persistence/cookie-persistence_test.go @@ -0,0 +1,331 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e_parallel + +package cookiepersistence + +import ( + "net/http" + "strings" + "testing" + + parser "github.com/haproxytech/client-native/v5/config-parser" + "github.com/haproxytech/client-native/v5/config-parser/options" + + "github.com/haproxytech/kubernetes-ingress/deploy/tests/e2e" + "github.com/stretchr/testify/suite" +) + +// Adding CookiePersistenceTest, just to be able to debug directly here and not from CRDTCPSuite +type CookiePersistenceTestSuite struct { + CookiePersistenceSuite +} + +func TestCookiePersistenceTestSuite(t *testing.T) { + suite.Run(t, new(CookiePersistenceTestSuite)) +} + +// Expected backend +// backend e2e-tests-cookie-persistence_http-echo_http +// ... +// cookie mycookie dynamic indirect nocache insert +// dynamic-cookie-key ohph7OoGhong +// server SRV_1 10.244.0.9:8888 enabled +// ... + +func (suite *CookiePersistenceTestSuite) Test_CookiePersistence_Dynamic() { + //------------------------ + // First step : Dynamic + suite.tmplData.CookiePersistenceDynamic = true + suite.tmplData.CookiePersistenceNoDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=f8f1bc84b3d0d5c0; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + cookies := res.Header["Set-Cookie"] + cookieOK := false + if len(cookies) != 0 { + for _, cookie := range cookies { + if strings.Contains(cookie, "mycookie") { + cookieOK = true + break + } + } + } + + return res.StatusCode == http.StatusOK && cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err := suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().Contains(cfg, "cookie mycookie dynamic indirect nocache insert") + suite.Require().Contains(cfg, "dynamic-cookie-key") + + // Check that the server line does not contain "cookie" param + reader := strings.NewReader(cfg) + p, err := parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + beName := suite.test.GetNS() + "_http-echo_http" + serverName := "SRV_1" + + suite.checkServerNoCookie(p, beName, serverName) + + // ------------------------ + // Second step : remove annotation + suite.tmplData.CookiePersistenceDynamic = false + suite.tmplData.CookiePersistenceNoDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=f8f1bc84b3d0d5c0; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + _, cookieOK := res.Header["Set-Cookie"] + + return res.StatusCode == http.StatusOK && !cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err = suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().NotContains(cfg, "cookie mycookie dynamic indirect nocache insert") + suite.Require().NotContains(cfg, "dynamic-cookie-key") + // Check that the server line does not contain "cookie" param + reader = strings.NewReader(cfg) + p, err = parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + + suite.checkServerNoCookie(p, beName, serverName) +} + +// Expected backend +// backend e2e-tests-cookie-persistence_http-echo_http +// ... +// cookie mycookie indirect nocache insert +// server SRV_1 10.244.0.13:8888 enabled cookie SRV_1 +// ... + +func (suite *CookiePersistenceTestSuite) Test_CookiePersistence_No_Dynamic() { + suite.tmplData.CookiePersistenceNoDynamic = true + suite.tmplData.CookiePersistenceDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + cookies := res.Header["Set-Cookie"] + cookieOK := false + if len(cookies) != 0 { + for _, cookie := range cookies { + if strings.Contains(cookie, "mycookie") && strings.Contains(cookie, "SRV_1") { + cookieOK = true + break + } + } + } + + return res.StatusCode == http.StatusOK && cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err := suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().Contains(cfg, "cookie mycookie indirect nocache insert") // NOTE that it does not contains dynamic + suite.Require().NotContains(cfg, "dynamic-cookie-key") + + reader := strings.NewReader(cfg) + p, err := parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + + // Check that the server line + beName := suite.test.GetNS() + "_http-echo_http" + serverName := "SRV_1" + + suite.checkServerCookie(p, beName, serverName) + + // ------------------------ + // Second step : remove annotation + suite.tmplData.CookiePersistenceDynamic = false + suite.tmplData.CookiePersistenceNoDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=f8f1bc84b3d0d5c0; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + _, cookieOK := res.Header["Set-Cookie"] + + return res.StatusCode == http.StatusOK && !cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err = suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().NotContains(cfg, "cookie mycookie dynamic indirect nocache insert") + suite.Require().NotContains(cfg, "dynamic-cookie-key") + // Check that the server line does not contain "cookie" param + reader = strings.NewReader(cfg) + p, err = parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + + suite.checkServerNoCookie(p, beName, serverName) +} + +func (suite *CookiePersistenceTestSuite) Test_CookiePersistence_Switch() { + //--------------------------- + // Step 1 : Dynamic + suite.tmplData.CookiePersistenceDynamic = true + suite.tmplData.CookiePersistenceNoDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=f8f1bc84b3d0d5c0; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + cookies := res.Header["Set-Cookie"] + cookieOK := false + if len(cookies) != 0 { + for _, cookie := range cookies { + if strings.Contains(cookie, "mycookie") { + cookieOK = true + break + } + } + } + + return res.StatusCode == http.StatusOK && cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err := suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().Contains(cfg, "cookie mycookie dynamic indirect nocache insert") + suite.Require().Contains(cfg, "dynamic-cookie-key") + + // Check that the server line does not contain "cookie" param + reader := strings.NewReader(cfg) + p, err := parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + beName := suite.test.GetNS() + "_http-echo_http" + serverName := "SRV_1" + + suite.checkServerNoCookie(p, beName, serverName) + + //---------------------- + // Step 2: not dynamic + suite.tmplData.CookiePersistenceNoDynamic = true + suite.tmplData.CookiePersistenceDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + cookies := res.Header["Set-Cookie"] + cookieOK := false + if len(cookies) != 0 { + for _, cookie := range cookies { + if strings.Contains(cookie, "mycookie") && strings.Contains(cookie, "SRV_1") { + cookieOK = true + break + } + } + } + + return res.StatusCode == http.StatusOK && cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err = suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().Contains(cfg, "cookie mycookie indirect nocache insert") // NOTE that it does not contains dynamic + suite.Require().NotContains(cfg, "dynamic-cookie-key") + + reader = strings.NewReader(cfg) + p, err = parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + + // Check that the server line + suite.checkServerCookie(p, beName, serverName) + + //------------------------ + // Step 3: and back: Dynamic + suite.tmplData.CookiePersistenceDynamic = true + suite.tmplData.CookiePersistenceNoDynamic = false + suite.Require().NoError(suite.test.Apply("config/deploy.yml.tmpl", suite.test.GetNS(), suite.tmplData)) + // Check that curl backend return 200 and "Set-Cookie ""mycookie=f8f1bc84b3d0d5c0; path=/" + suite.Eventually(func() bool { + res, cls, err := suite.client.Do() + if res == nil { + suite.T().Log(err) + return false + } + defer cls() + cookies := res.Header["Set-Cookie"] + cookieOK := false + if len(cookies) != 0 { + for _, cookie := range cookies { + if strings.Contains(cookie, "mycookie") { + cookieOK = true + break + } + } + } + + return res.StatusCode == http.StatusOK && cookieOK + }, e2e.WaitDuration, e2e.TickDuration) + + // Also check configuration + cfg, err = suite.test.GetIngressControllerFile("/etc/haproxy/haproxy.cfg") + suite.Require().NoError(err, "Could not get Haproxy config") + + suite.Require().Contains(cfg, "cookie mycookie dynamic indirect nocache insert") + suite.Require().Contains(cfg, "dynamic-cookie-key") + + // Check that the server line does not contain "cookie" param + reader = strings.NewReader(cfg) + p, err = parser.New(options.Reader(reader)) + suite.Require().NoError(err, "Could not get Haproxy config parser") + + suite.checkServerNoCookie(p, beName, serverName) +} diff --git a/deploy/tests/e2e/cookie-persistence/suite_test.go b/deploy/tests/e2e/cookie-persistence/suite_test.go new file mode 100644 index 00000000..0238e955 --- /dev/null +++ b/deploy/tests/e2e/cookie-persistence/suite_test.go @@ -0,0 +1,122 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e_parallel + +package cookiepersistence + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + parser "github.com/haproxytech/client-native/v5/config-parser" + "github.com/haproxytech/client-native/v5/config-parser/params" + "github.com/haproxytech/client-native/v5/config-parser/types" + + "github.com/haproxytech/kubernetes-ingress/deploy/tests/e2e" +) + +type CookiePersistenceSuite struct { + suite.Suite + test e2e.Test + client *e2e.Client + tmplData tmplData +} + +type tmplData struct { + CookiePersistenceDynamic bool + CookiePersistenceNoDynamic bool + Host string +} + +func (suite *CookiePersistenceSuite) SetupSuite() { + var err error + suite.test, err = e2e.NewTest() + suite.Require().NoError(err) + suite.tmplData = tmplData{Host: suite.test.GetNS() + ".test"} + suite.client, err = e2e.NewHTTPClient(suite.tmplData.Host) + suite.Require().NoError(err) +} + +func (suite *CookiePersistenceSuite) TearDownSuite() { + suite.test.TearDown() +} + +func TestCookiePersistenceSuite(t *testing.T) { + suite.Run(t, new(CookiePersistenceSuite)) +} + +// Check that the server serverName for backend backendName +// has a "cookie" params with a value equal to the server name +func (suite *CookiePersistenceSuite) checkServerCookie(p parser.Parser, backendName, serverName string) { + v, err := p.Get(parser.Backends, backendName, "server") + suite.Require().NoError(err, "Could not get Haproxy config parser servers for backend %s", backendName) + + ondiskServers, ok := v.([]types.Server) + suite.Require().Equal(ok, true, "Could not get Haproxy config parser servers for backend %s", backendName) + + paramName := "cookie" + var cookieParam *params.ServerOptionValue + for _, server := range ondiskServers { + if server.Name != serverName { + continue + } + serverParams := server.Params + for _, serverParam := range serverParams { + optionValue, ok := serverParam.(*params.ServerOptionValue) + if !ok { + continue + } + if optionValue.Name != paramName { + continue + } + cookieParam = optionValue + break + } + } + suite.Require().NotNil(cookieParam) + suite.Require().Equal(cookieParam.Value, serverName) +} + +// Check that the server serverName for backend backendName +// has a NOT a "cookie" params +func (suite *CookiePersistenceSuite) checkServerNoCookie(p parser.Parser, backendName, serverName string) { + v, err := p.Get(parser.Backends, backendName, "server") + suite.Require().NoError(err, "Could not get Haproxy config parser servers for backend %s", backendName) + + ondiskServers, ok := v.([]types.Server) + suite.Require().Equal(ok, true, "Could not get Haproxy config parser servers for backend %s", backendName) + + paramName := "cookie" + cookieParamFound := false + for _, server := range ondiskServers { + if server.Name != serverName { + continue + } + serverParams := server.Params + for _, serverParam := range serverParams { + optionValue, ok := serverParam.(*params.ServerOptionValue) + if !ok { + continue + } + if optionValue.Name != paramName { + continue + } + cookieParamFound = true + break + } + } + suite.Require().Equal(cookieParamFound, false) +} diff --git a/documentation/annotations.md b/documentation/annotations.md index f0527fb4..08c8d00d 100644 --- a/documentation/annotations.md +++ b/documentation/annotations.md @@ -35,6 +35,7 @@ This is autogenerated from [doc.yaml](doc.yaml). Description can be found in [ge | [stats-config-snippet](#config-snippet) | string | | |:large_blue_circle:|:white_circle:|:white_circle:| | [backend-config-snippet](#config-snippet) | string | | |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:| | [cookie-persistence](#cookie-persistence) | string | | |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:| +| [cookie-persistence-no-dynamic](#cookie-persistence-no-dynamic) :construction:(dev) | string | | |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:| | [dontlognull](#logging) | [bool](#bool) | "true" | |:large_blue_circle:|:white_circle:|:white_circle:| | [src-ip-header](#src-ip-header) | string | "null" | |:large_blue_circle:|:large_blue_circle:|:white_circle:| | [forwarded-for](#x-forwarded-for) | [bool](#bool) | "true" | |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:| @@ -702,6 +703,36 @@ cookie-persistence: "mycookie" *** +#### Cookie Persistence No Dynamic + +##### `cookie-persistence-no-dynamic` + + + > :construction: this is only available from next version, currently available in dev build + + Enables persistent connections (sticky sessions) between a client and a pod by inserting a cookie into the client's browser that is used to remember which backend pod they connected to before. + Dynamic cookies are not used contrary to cookie-persistence annotation. The cookie will have the server name. + + Available on: `configmap` `ingress` `service` + + :information_source: This will insert the following cookie configuration in the corresponding backend +`cookie indirect nocache insert` with `` the value of this annotation. +The server line will have `server enabled cookie ` + +Possible values: + +- A name for the cookie + +Example: + +```yaml +cookie-persistence-no-dynamic: "mycookie" +``` + +

:arrow_up_small: back to top

+ +*** + #### Hard Stop After ##### `hard-stop-after` diff --git a/documentation/doc.yaml b/documentation/doc.yaml index dadaf68f..2deac235 100644 --- a/documentation/doc.yaml +++ b/documentation/doc.yaml @@ -871,6 +871,26 @@ annotations: - service version_min: "1.4" example: ['cookie-persistence: "mycookie"'] + - title: cookie-persistence-no-dynamic + type: string + description: + - Enables persistent connections (sticky sessions) between a client and a pod by inserting a cookie + into the client's browser that is used to remember which backend pod they connected + to before. + - Dynamic cookies are not used contrary to cookie-persistence annotation. The cookie will have the server name. + tip: + - |- + This will insert the following cookie configuration in the corresponding backend + `cookie indirect nocache insert` with `` the value of this annotation. + The server line will have `server enabled cookie ` + values: + - A name for the cookie + applies_to: + - configmap + - ingress + - service + version_min: "3.1" + example: ['cookie-persistence-no-dynamic: "mycookie"'] - title: dontlognull type: bool group: logging diff --git a/pkg/annotations/service/cookie.go b/pkg/annotations/service/cookie.go index 383c5a59..b275a7c3 100644 --- a/pkg/annotations/service/cookie.go +++ b/pkg/annotations/service/cookie.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "strings" "github.com/haproxytech/client-native/v5/models" @@ -9,13 +10,20 @@ import ( "github.com/haproxytech/kubernetes-ingress/pkg/store" ) +//nolint:stylecheck +const ( + SUFFIX_NO_DYNAMIC = "no-dynamic" +) + type Cookie struct { - backend *models.Backend - name string + backend *models.Backend + name string + nameNoDynamic string } func NewCookie(n string, b *models.Backend) *Cookie { - return &Cookie{name: n, backend: b} + nameNoDynamic := n + "-" + SUFFIX_NO_DYNAMIC + return &Cookie{name: n, nameNoDynamic: nameNoDynamic, backend: b} } func (a *Cookie) GetName() string { @@ -23,19 +31,38 @@ func (a *Cookie) GetName() string { } func (a *Cookie) Process(k store.K8s, annotations ...map[string]string) error { + // Cookie dynamic annotation ? input := common.GetValue(a.GetName(), annotations...) params := strings.Fields(input) - if len(params) == 0 { + + // Is there a "no-dynamic" annotation + inputNoDynamic := common.GetValue(a.nameNoDynamic, annotations...) + paramsNoDynamic := strings.Fields(inputNoDynamic) + + if len(paramsNoDynamic) > 0 && len(params) > 0 { + return fmt.Errorf("cookie: cannot use both %s and %s annotations", a.GetName(), a.nameNoDynamic) + } + + if len(params) == 0 && len(paramsNoDynamic) == 0 { a.backend.Cookie = nil return nil } - cookieName := params[0] + + cookieName := "" + isdynamicCookie := true + if len(params) > 0 { + cookieName = params[0] + } else { + cookieName = paramsNoDynamic[0] + isdynamicCookie = false + } + a.backend.Cookie = &models.Cookie{ Name: &cookieName, Type: "insert", Nocache: true, Indirect: true, - Dynamic: true, + Dynamic: isdynamicCookie, Domains: []*models.Domain{}, } return nil diff --git a/pkg/service/endpoints.go b/pkg/service/endpoints.go index 32c57623..83ba0202 100644 --- a/pkg/service/endpoints.go +++ b/pkg/service/endpoints.go @@ -48,7 +48,7 @@ func (s *Service) HandleHAProxySrvs(k8s store.K8s, client api.HAProxyClient) { } // update servers for _, srvSlot := range backend.HAProxySrvs { - if srvSlot.Modified || s.newBackend { + if srvSlot.Modified || s.newBackend || s.serversToEdit { s.updateHAProxySrv(client, *srvSlot, backend.Endpoints.Port) } } @@ -66,6 +66,9 @@ func (s *Service) updateHAProxySrv(client api.HAProxyClient, srvSlot store.HAPro Address: "127.0.0.1", ServerParams: models.ServerParams{Maintenance: "enabled"}, } + if s.backend.Cookie != nil && !s.backend.Cookie.Dynamic { + srv.ServerParams.Cookie = srvSlot.Name + } // Enable Server if srvSlot.Address != "" { srv.Address = srvSlot.Address diff --git a/pkg/service/service.go b/pkg/service/service.go index 54aa4bdc..3e0e170a 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -44,11 +44,12 @@ type Service struct { backend *models.Backend // ingressName string // ingressNamespace string - ingress *store.Ingress - annotations []map[string]string - modeTCP bool - newBackend bool - standalone bool + ingress *store.Ingress + annotations []map[string]string + modeTCP bool + newBackend bool + standalone bool + serversToEdit bool } // New returns a Service instance to handle the k8s IngressPath resource given in params. @@ -76,8 +77,6 @@ func New(k store.K8s, path *store.IngressPath, certs certs.Certificates, tcpServ modeTCP: tcpService, standalone: standalone, ingress: ingress, - // ingressName: ingressName, - // ingressNamespace: ingressNamespace, }, nil } @@ -156,6 +155,10 @@ func (s *Service) HandleBackend(storeK8s store.K8s, client api.HAProxyClient, a // Update Backend diff := newBackend.Config.Diff(*backend) if len(diff) != 0 { + // Detect if we have a diff on the server line + if isServersToEdit(newBackend.Config, backend) { + s.serversToEdit = true + } if err = client.BackendEdit(*newBackend.Config); err != nil { return } @@ -184,6 +187,21 @@ func (s *Service) HandleBackend(storeK8s store.K8s, client api.HAProxyClient, a return } +func isServersToEdit(oldBackend *models.Backend, newBackend *models.Backend) bool { + // Detect if we have a diff on the server line + newCookie := newBackend.Cookie + oldCookie := oldBackend.Cookie + var cookieAreDifferent bool + // Both are nil + if newCookie == nil || oldCookie == nil { + cookieAreDifferent = !(newCookie == oldCookie) + return cookieAreDifferent + } + + cookieAreDifferent = len(newCookie.Diff(*oldCookie)) > 0 + return cookieAreDifferent +} + // getBackendModel checks for a corresponding custom resource before falling back to annotations func (s *Service) getBackendModel(store store.K8s, a annotations.Annotations) (backend *v1.BackendSpec, err error) { // Backend mode