Enable session affinity for canaries (#7371)

This commit is contained in:
wasker 2021-07-29 14:23:19 -07:00 committed by GitHub
parent a327a809d9
commit f222c752be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1021 additions and 322 deletions

View file

@ -30,19 +30,20 @@ import (
)
var (
annotationPassthrough = parser.GetAnnotationWithPrefix("ssl-passthrough")
annotationAffinityType = parser.GetAnnotationWithPrefix("affinity")
annotationAffinityMode = parser.GetAnnotationWithPrefix("affinity-mode")
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
annotationPassthrough = parser.GetAnnotationWithPrefix("ssl-passthrough")
annotationAffinityType = parser.GetAnnotationWithPrefix("affinity")
annotationAffinityMode = parser.GetAnnotationWithPrefix("affinity-mode")
annotationAffinityCanaryBehavior = parser.GetAnnotationWithPrefix("affinity-canary-behavior")
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
)
type mockCfg struct {
@ -162,29 +163,38 @@ func TestAffinitySession(t *testing.T) {
ing := buildIngress()
fooAnns := []struct {
annotations map[string]string
affinitytype string
affinitymode string
name string
annotations map[string]string
affinitytype string
affinitymode string
cookiename string
canarybehavior string
}{
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: "route"}, "cookie", "balanced", "route"},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "persistent", annotationAffinityCookieName: "route1"}, "cookie", "persistent", "route1"},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: ""}, "cookie", "balanced", "INGRESSCOOKIE"},
{map[string]string{}, "", "", ""},
{nil, "", "", ""},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: "route", annotationAffinityCanaryBehavior: ""}, "cookie", "balanced", "route", ""},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "persistent", annotationAffinityCookieName: "route1", annotationAffinityCanaryBehavior: "sticky"}, "cookie", "persistent", "route1", "sticky"},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: "", annotationAffinityCanaryBehavior: "legacy"}, "cookie", "balanced", "INGRESSCOOKIE", "legacy"},
{map[string]string{}, "", "", "", ""},
{nil, "", "", "", ""},
}
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Extract(ing).SessionAffinity
t.Logf("Testing pass %v %v", foo.affinitytype, foo.name)
t.Logf("Testing pass %v %v", foo.affinitytype, foo.cookiename)
if r.Mode != foo.affinitymode {
t.Errorf("Returned %v but expected %v for Name", r.Mode, foo.affinitymode)
if r.Type != foo.affinitytype {
t.Errorf("Returned %v but expected %v for Type", r.Type, foo.affinitytype)
}
if r.Cookie.Name != foo.name {
t.Errorf("Returned %v but expected %v for Name", r.Cookie.Name, foo.name)
if r.Mode != foo.affinitymode {
t.Errorf("Returned %v but expected %v for Mode", r.Mode, foo.affinitymode)
}
if r.CanaryBehavior != foo.canarybehavior {
t.Errorf("Returned %v but expected %v for CanaryBehavior", r.CanaryBehavior, foo.canarybehavior)
}
if r.Cookie.Name != foo.cookiename {
t.Errorf("Returned %v but expected %v for Cookie.Name", r.Cookie.Name, foo.cookiename)
}
}
}

View file

@ -27,8 +27,10 @@ import (
)
const (
annotationAffinityType = "affinity"
annotationAffinityMode = "affinity-mode"
annotationAffinityType = "affinity"
annotationAffinityMode = "affinity-mode"
annotationAffinityCanaryBehavior = "affinity-canary-behavior"
// If a cookie with this name exists,
// its value is used as an index into the list of available backends.
annotationAffinityCookieName = "session-cookie-name"
@ -66,6 +68,8 @@ type Config struct {
Type string `json:"type"`
// The affinity mode, i.e. how sticky a session is
Mode string `json:"mode"`
// Affinity behavior for canaries (sticky or legacy)
CanaryBehavior string `json:"canaryBehavior"`
Cookie
}
@ -160,6 +164,11 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
am = ""
}
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing)
if err != nil {
cb = ""
}
switch at {
case "cookie":
cookie = a.cookieAffinityParse(ing)
@ -169,8 +178,9 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
}
return &Config{
Type: at,
Mode: am,
Cookie: *cookie,
Type: at,
Mode: am,
CanaryBehavior: cb,
Cookie: *cookie,
}, nil
}

View file

@ -1315,7 +1315,7 @@ func canMergeBackend(primary *ingress.Backend, alternative *ingress.Backend) boo
}
// Performs the merge action and checks to ensure that one two alternative backends do not merge into each other
func mergeAlternativeBackend(priUps *ingress.Backend, altUps *ingress.Backend) bool {
func mergeAlternativeBackend(ing *ingress.Ingress, priUps *ingress.Backend, altUps *ingress.Backend) bool {
if priUps.NoServer {
klog.Warningf("unable to merge alternative backend %v into primary backend %v because %v is a primary backend",
altUps.Name, priUps.Name, priUps.Name)
@ -1329,6 +1329,10 @@ func mergeAlternativeBackend(priUps *ingress.Backend, altUps *ingress.Backend) b
}
}
if ing.ParsedAnnotations != nil && ing.ParsedAnnotations.SessionAffinity.CanaryBehavior != "legacy" {
priUps.SessionAffinity.DeepCopyInto(&altUps.SessionAffinity)
}
priUps.AlternativeBackends =
append(priUps.AlternativeBackends, altUps.Name)
@ -1368,7 +1372,7 @@ func mergeAlternativeBackends(ing *ingress.Ingress, upstreams map[string]*ingres
klog.V(2).Infof("matching backend %v found for alternative backend %v",
priUps.Name, altUps.Name)
merged = mergeAlternativeBackend(priUps, altUps)
merged = mergeAlternativeBackend(ing, priUps, altUps)
}
}
@ -1421,7 +1425,7 @@ func mergeAlternativeBackends(ing *ingress.Ingress, upstreams map[string]*ingres
klog.V(2).Infof("matching backend %v found for alternative backend %v",
priUps.Name, altUps.Name)
merged = mergeAlternativeBackend(priUps, altUps)
merged = mergeAlternativeBackend(ing, priUps, altUps)
}
}

View file

@ -44,6 +44,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/canary"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl"
"k8s.io/ingress-nginx/internal/ingress/annotations/sessionaffinity"
"k8s.io/ingress-nginx/internal/ingress/controller/config"
ngx_config "k8s.io/ingress-nginx/internal/ingress/controller/config"
"k8s.io/ingress-nginx/internal/ingress/controller/store"
@ -786,6 +787,326 @@ func TestMergeAlternativeBackends(t *testing.T) {
},
},
},
"alternative backend gets SessionAffinitySettings configured when CanaryBehavior is 'sticky'": {
&ingress.Ingress{
Ingress: networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Namespace: "example",
},
Spec: networking.IngressSpec{
Rules: []networking.IngressRule{
{
Host: "example.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: networking.IngressBackend{
ServiceName: "http-svc-canary",
ServicePort: intstr.IntOrString{
IntVal: 80,
},
},
},
},
},
},
},
},
},
},
ParsedAnnotations: &annotations.Ingress{
SessionAffinity: sessionaffinity.Config{
CanaryBehavior: "sticky",
},
},
},
map[string]*ingress.Backend{
"example-http-svc-80": {
Name: "example-http-svc-80",
NoServer: false,
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
"example-http-svc-canary-80": {
Name: "example-http-svc-canary-80",
NoServer: true,
TrafficShapingPolicy: ingress.TrafficShapingPolicy{
Weight: 20,
},
},
},
map[string]*ingress.Server{
"example.com": {
Hostname: "example.com",
Locations: []*ingress.Location{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: "example-http-svc-80",
},
},
},
},
map[string]*ingress.Backend{
"example-http-svc-80": {
Name: "example-http-svc-80",
NoServer: false,
AlternativeBackends: []string{"example-http-svc-canary-80"},
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
"example-http-svc-canary-80": {
Name: "example-http-svc-canary-80",
NoServer: true,
TrafficShapingPolicy: ingress.TrafficShapingPolicy{
Weight: 20,
},
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
},
map[string]*ingress.Server{
"example.com": {
Hostname: "example.com",
Locations: []*ingress.Location{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: "example-http-svc-80",
},
},
},
},
},
"alternative backend gets SessionAffinitySettings configured when CanaryBehavior is not 'legacy'": {
&ingress.Ingress{
Ingress: networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Namespace: "example",
},
Spec: networking.IngressSpec{
Rules: []networking.IngressRule{
{
Host: "example.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: networking.IngressBackend{
ServiceName: "http-svc-canary",
ServicePort: intstr.IntOrString{
IntVal: 80,
},
},
},
},
},
},
},
},
},
},
ParsedAnnotations: &annotations.Ingress{
SessionAffinity: sessionaffinity.Config{
CanaryBehavior: "", // In fact any value but 'legacy' would do the trick.
},
},
},
map[string]*ingress.Backend{
"example-http-svc-80": {
Name: "example-http-svc-80",
NoServer: false,
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
"example-http-svc-canary-80": {
Name: "example-http-svc-canary-80",
NoServer: true,
TrafficShapingPolicy: ingress.TrafficShapingPolicy{
Weight: 20,
},
},
},
map[string]*ingress.Server{
"example.com": {
Hostname: "example.com",
Locations: []*ingress.Location{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: "example-http-svc-80",
},
},
},
},
map[string]*ingress.Backend{
"example-http-svc-80": {
Name: "example-http-svc-80",
NoServer: false,
AlternativeBackends: []string{"example-http-svc-canary-80"},
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
"example-http-svc-canary-80": {
Name: "example-http-svc-canary-80",
NoServer: true,
TrafficShapingPolicy: ingress.TrafficShapingPolicy{
Weight: 20,
},
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
},
map[string]*ingress.Server{
"example.com": {
Hostname: "example.com",
Locations: []*ingress.Location{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: "example-http-svc-80",
},
},
},
},
},
"alternative backend doesn't get SessionAffinitySettings configured when CanaryBehavior is 'legacy'": {
&ingress.Ingress{
Ingress: networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Namespace: "example",
},
Spec: networking.IngressSpec{
Rules: []networking.IngressRule{
{
Host: "example.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: networking.IngressBackend{
ServiceName: "http-svc-canary",
ServicePort: intstr.IntOrString{
IntVal: 80,
},
},
},
},
},
},
},
},
},
},
ParsedAnnotations: &annotations.Ingress{
SessionAffinity: sessionaffinity.Config{
CanaryBehavior: "legacy",
},
},
},
map[string]*ingress.Backend{
"example-http-svc-80": {
Name: "example-http-svc-80",
NoServer: false,
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
"example-http-svc-canary-80": {
Name: "example-http-svc-canary-80",
NoServer: true,
TrafficShapingPolicy: ingress.TrafficShapingPolicy{
Weight: 20,
},
},
},
map[string]*ingress.Server{
"example.com": {
Hostname: "example.com",
Locations: []*ingress.Location{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: "example-http-svc-80",
},
},
},
},
map[string]*ingress.Backend{
"example-http-svc-80": {
Name: "example-http-svc-80",
NoServer: false,
AlternativeBackends: []string{"example-http-svc-canary-80"},
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
AffinityMode: "balanced",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Name: "test",
},
},
},
"example-http-svc-canary-80": {
Name: "example-http-svc-canary-80",
NoServer: true,
TrafficShapingPolicy: ingress.TrafficShapingPolicy{
Weight: 20,
},
},
},
map[string]*ingress.Server{
"example.com": {
Hostname: "example.com",
Locations: []*ingress.Location{
{
Path: "/",
PathType: &pathTypePrefix,
Backend: "example-http-svc-80",
},
},
},
},
},
}
for title, tc := range testCases {
@ -801,7 +1122,7 @@ func TestMergeAlternativeBackends(t *testing.T) {
if !actualUpstream.Equal(expUpstream) {
t.Logf("actual upstream %s alternative backends: %s", actualUpstream.Name, actualUpstream.AlternativeBackends)
t.Logf("expected upstream %s alternative backends: %s", expUpstream.Name, expUpstream.AlternativeBackends)
t.Errorf("upstream %s was not equal to what was expected: ", upsName)
t.Errorf("upstream %s was not equal to what was expected", actualUpstream.Name)
}
}