Enable session affinity for canaries (#7371)
This commit is contained in:
parent
a327a809d9
commit
f222c752be
17 changed files with 1021 additions and 322 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue