From 83d03a19a662496c47cb1cf9ec451d35be09413a Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Thu, 15 Jun 2017 20:43:17 -0400 Subject: [PATCH] Add feature to allow sticky sessions per location --- controllers/nginx/pkg/template/template.go | 46 ++++++++++++++++++- .../nginx/pkg/template/template_test.go | 2 +- .../rootfs/etc/nginx/template/nginx.tmpl | 26 +++++++---- core/pkg/ingress/controller/controller.go | 7 +++ core/pkg/ingress/controller/util.go | 5 ++ core/pkg/ingress/types.go | 5 +- 6 files changed, 77 insertions(+), 14 deletions(-) diff --git a/controllers/nginx/pkg/template/template.go b/controllers/nginx/pkg/template/template.go index 1bdf0b8aa4..482497310d 100644 --- a/controllers/nginx/pkg/template/template.go +++ b/controllers/nginx/pkg/template/template.go @@ -135,6 +135,7 @@ var ( "buildRateLimitZones": buildRateLimitZones, "buildRateLimit": buildRateLimit, "buildResolvers": buildResolvers, + "buildUpstreamName": buildUpstreamName, "isLocationAllowed": isLocationAllowed, "buildLogFormatUpstream": buildLogFormatUpstream, "buildDenyVariable": buildDenyVariable, @@ -257,7 +258,7 @@ func buildLogFormatUpstream(input interface{}) string { // (specified through the ingress.kubernetes.io/rewrite-to annotation) // If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will // add a base tag in the head of the response from the service -func buildProxyPass(b interface{}, loc interface{}) string { +func buildProxyPass(host string, b interface{}, loc interface{}) string { backends := b.([]*ingress.Backend) location, ok := loc.(*ingress.Location) if !ok { @@ -267,17 +268,23 @@ func buildProxyPass(b interface{}, loc interface{}) string { path := location.Path proto := "http" + upstreamName := location.Backend for _, backend := range backends { if backend.Name == location.Backend { if backend.Secure || backend.SSLPassthrough { proto = "https" } + + if isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) { + upstreamName = fmt.Sprintf("sticky-%v", upstreamName) + } + break } } // defProxyPass returns the default proxy_pass, just the name of the upstream - defProxyPass := fmt.Sprintf("proxy_pass %s://%s;", proto, location.Backend) + defProxyPass := fmt.Sprintf("proxy_pass %s://%s;", proto, upstreamName) // if the path in the ingress rule is equals to the target: no special rewrite if path == location.Redirect.Target { return defProxyPass @@ -408,3 +415,38 @@ func buildDenyVariable(a interface{}) string { return fmt.Sprintf("$deny_%v", denyPathSlugMap[l]) } + +func buildUpstreamName(host string, b interface{}, loc interface{}) string { + backends := b.([]*ingress.Backend) + location, ok := loc.(*ingress.Location) + if !ok { + return "" + } + + upstreamName := location.Backend + + for _, backend := range backends { + if backend.Name == location.Backend { + if backend.SessionAffinity.AffinityType == "cookie" && + isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) { + upstreamName = fmt.Sprintf("sticky-%v", upstreamName) + } + + break + } + } + + return upstreamName +} + +func isSticky(host string, loc *ingress.Location, stickyLocations map[string][]string) bool { + if _, ok := stickyLocations[host]; ok { + for _, sl := range stickyLocations[host] { + if sl == loc.Path { + return true + } + } + } + + return false +} diff --git a/controllers/nginx/pkg/template/template_test.go b/controllers/nginx/pkg/template/template_test.go index d0eac50fa9..ac8a1b5c7b 100644 --- a/controllers/nginx/pkg/template/template_test.go +++ b/controllers/nginx/pkg/template/template_test.go @@ -129,7 +129,7 @@ func TestBuildProxyPass(t *testing.T) { Backend: "upstream-name", } - pp := buildProxyPass([]*ingress.Backend{}, loc) + pp := buildProxyPass("", []*ingress.Backend{}, loc) if !strings.EqualFold(tc.ProxyPass, pp) { t.Errorf("%s: expected \n'%v'\nbut returned \n'%v'", k, tc.ProxyPass, pp) } diff --git a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl index f78aed02a4..04bd1ae6b9 100644 --- a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl +++ b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl @@ -225,16 +225,25 @@ http { proxy_pass_header Server; {{ end }} - {{range $name, $upstream := $backends}} - upstream {{$upstream.Name}} { - {{ if eq $upstream.SessionAffinity.AffinityType "cookie" }} - sticky hash={{$upstream.SessionAffinity.CookieSessionAffinity.Hash}} name={{$upstream.SessionAffinity.CookieSessionAffinity.Name}} httponly; - {{ else }} + {{ range $name, $upstream := $backends }} + {{ if eq $upstream.SessionAffinity.AffinityType "cookie" }} + upstream sticky-{{ $upstream.Name }} { + sticky hash={{ $upstream.SessionAffinity.CookieSessionAffinity.Hash }} name={{ $upstream.SessionAffinity.CookieSessionAffinity.Name }} httponly; + + {{ if (gt $cfg.UpstreamKeepaliveConnections 0) }} + keepalive {{ $cfg.UpstreamKeepaliveConnections }}; + {{ end }} + + {{ range $server := $upstream.Endpoints }}server {{ $server.Address | formatIP }}:{{ $server.Port }} max_fails={{ $server.MaxFails }} fail_timeout={{ $server.FailTimeout }}; + {{ end }} + } + {{ end }} + + upstream {{ $upstream.Name }} { # Load balance algorithm; empty for round robin, which is the default {{ if ne $cfg.LoadBalanceAlgorithm "round_robin" }} {{ $cfg.LoadBalanceAlgorithm }}; {{ end }} - {{ end }} {{ if (gt $cfg.UpstreamKeepaliveConnections 0) }} keepalive {{ $cfg.UpstreamKeepaliveConnections }}; @@ -343,8 +352,7 @@ http { {{ end }} location {{ $path }} { - set $proxy_upstream_name "{{ $location.Backend }}"; - + set $proxy_upstream_name "{{ buildUpstreamName $server.Hostname $backends $location }}"; {{ if isLocationAllowed $location }} {{ if gt (len $location.Whitelist.CIDR) 0 }} if ({{ buildDenyVariable (print $server.Hostname "_" $path) }}) { @@ -439,7 +447,7 @@ http { {{/* Add any additional configuration defined */}} {{ $location.ConfigurationSnippet }} - {{ buildProxyPass $backends $location }} + {{ buildProxyPass $server.Hostname $backends $location }} {{ else }} #{{ $location.Denied }} return 503; diff --git a/core/pkg/ingress/controller/controller.go b/core/pkg/ingress/controller/controller.go index be4ff6a325..449783b6bb 100644 --- a/core/pkg/ingress/controller/controller.go +++ b/core/pkg/ingress/controller/controller.go @@ -791,14 +791,21 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing if !upstreams[name].Secure { upstreams[name].Secure = secUpstream.Secure } + if upstreams[name].SecureCACert.Secret == "" { upstreams[name].SecureCACert = secUpstream.CACert } + if upstreams[name].SessionAffinity.AffinityType == "" { upstreams[name].SessionAffinity.AffinityType = affinity.AffinityType if affinity.AffinityType == "cookie" { upstreams[name].SessionAffinity.CookieSessionAffinity.Name = affinity.CookieConfig.Name upstreams[name].SessionAffinity.CookieSessionAffinity.Hash = affinity.CookieConfig.Hash + + if _, ok := upstreams[name].SessionAffinity.CookieSessionAffinity.Locations[rule.Host]; !ok { + upstreams[name].SessionAffinity.CookieSessionAffinity.Locations[rule.Host] = []string{} + } + upstreams[name].SessionAffinity.CookieSessionAffinity.Locations[rule.Host] = append(upstreams[name].SessionAffinity.CookieSessionAffinity.Locations[rule.Host], path.Path) } } diff --git a/core/pkg/ingress/controller/util.go b/core/pkg/ingress/controller/util.go index 4feb882ba3..d5c5ec8a42 100644 --- a/core/pkg/ingress/controller/util.go +++ b/core/pkg/ingress/controller/util.go @@ -39,6 +39,11 @@ func newUpstream(name string) *ingress.Backend { return &ingress.Backend{ Name: name, Endpoints: []ingress.Endpoint{}, + SessionAffinity: ingress.SessionAffinityConfig{ + CookieSessionAffinity: ingress.CookieSessionAffinity{ + Locations: make(map[string][]string), + }, + }, } } diff --git a/core/pkg/ingress/types.go b/core/pkg/ingress/types.go index 875259a1af..789c6c8244 100644 --- a/core/pkg/ingress/types.go +++ b/core/pkg/ingress/types.go @@ -174,8 +174,9 @@ type SessionAffinityConfig struct { // CookieSessionAffinity defines the structure used in Affinity configured by Cookies. type CookieSessionAffinity struct { - Name string `json:"name"` - Hash string `json:"hash"` + Name string `json:"name"` + Hash string `json:"hash"` + Locations map[string][]string `json:"locations,omitempty"` } // Endpoint describes a kubernetes endpoint in a backend