Enable session affinity for canaries (#7371)
This commit is contained in:
parent
a327a809d9
commit
f222c752be
17 changed files with 1021 additions and 322 deletions
|
@ -25,6 +25,9 @@ set -o pipefail
|
||||||
# temporal directory for the /etc/ingress-controller directory
|
# temporal directory for the /etc/ingress-controller directory
|
||||||
INGRESS_VOLUME=$(mktemp -d)
|
INGRESS_VOLUME=$(mktemp -d)
|
||||||
|
|
||||||
|
# make sure directory for SSL cert storage exists under ingress volume
|
||||||
|
mkdir "${INGRESS_VOLUME}/ssl"
|
||||||
|
|
||||||
if [[ "$OSTYPE" == darwin* ]]; then
|
if [[ "$OSTYPE" == darwin* ]]; then
|
||||||
INGRESS_VOLUME=/private$INGRESS_VOLUME
|
INGRESS_VOLUME=/private$INGRESS_VOLUME
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -10,6 +10,7 @@ Session affinity can be configured using the following annotations:
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|nginx.ingress.kubernetes.io/affinity|Type of the affinity, set this to `cookie` to enable session affinity|string (NGINX only supports `cookie`)|
|
|nginx.ingress.kubernetes.io/affinity|Type of the affinity, set this to `cookie` to enable session affinity|string (NGINX only supports `cookie`)|
|
||||||
|nginx.ingress.kubernetes.io/affinity-mode|The affinity mode defines how sticky a session is. Use `balanced` to redistribute some sessions when scaling pods or `persistent` for maximum stickiness.|`balanced` (default) or `persistent`|
|
|nginx.ingress.kubernetes.io/affinity-mode|The affinity mode defines how sticky a session is. Use `balanced` to redistribute some sessions when scaling pods or `persistent` for maximum stickiness.|`balanced` (default) or `persistent`|
|
||||||
|
|nginx.ingress.kubernetes.io/affinity-canary-behavior|Defines session affinity behavior of canaries. By default the behavior is `sticky`, and canaries respect session affinity configuration. Set this to `legacy` to restore original canary behavior, when session affinity parameters were not respected.|`sticky` (default) or `legacy`|
|
||||||
|nginx.ingress.kubernetes.io/session-cookie-name|Name of the cookie that will be created|string (defaults to `INGRESSCOOKIE`)|
|
|nginx.ingress.kubernetes.io/session-cookie-name|Name of the cookie that will be created|string (defaults to `INGRESSCOOKIE`)|
|
||||||
|nginx.ingress.kubernetes.io/session-cookie-path|Path that will be set on the cookie (required if your [Ingress paths][ingress-paths] use regular expressions)|string (defaults to the currently [matched path][ingress-paths])|
|
|nginx.ingress.kubernetes.io/session-cookie-path|Path that will be set on the cookie (required if your [Ingress paths][ingress-paths] use regular expressions)|string (defaults to the currently [matched path][ingress-paths])|
|
||||||
|nginx.ingress.kubernetes.io/session-cookie-samesite|SameSite attribute to apply to the cookie|Browser accepted values are `None`, `Lax`, and `Strict`|
|
|nginx.ingress.kubernetes.io/session-cookie-samesite|SameSite attribute to apply to the cookie|Browser accepted values are `None`, `Lax`, and `Strict`|
|
||||||
|
|
|
@ -18,6 +18,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|
||||||
|[nginx.ingress.kubernetes.io/app-root](#rewrite)|string|
|
|[nginx.ingress.kubernetes.io/app-root](#rewrite)|string|
|
||||||
|[nginx.ingress.kubernetes.io/affinity](#session-affinity)|cookie|
|
|[nginx.ingress.kubernetes.io/affinity](#session-affinity)|cookie|
|
||||||
|[nginx.ingress.kubernetes.io/affinity-mode](#session-affinity)|"balanced" or "persistent"|
|
|[nginx.ingress.kubernetes.io/affinity-mode](#session-affinity)|"balanced" or "persistent"|
|
||||||
|
|[nginx.ingress.kubernetes.io/affinity-canary-behavior](#session-affinity)|"sticky" or "legacy"|
|
||||||
|[nginx.ingress.kubernetes.io/auth-realm](#authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-realm](#authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-secret](#authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-secret](#authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-secret-type](#authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-secret-type](#authentication)|string|
|
||||||
|
@ -140,7 +141,7 @@ In some cases, you may want to "canary" a new set of changes by sending a small
|
||||||
Canary rules are evaluated in order of precedence. Precedence is as follows:
|
Canary rules are evaluated in order of precedence. Precedence is as follows:
|
||||||
`canary-by-header -> canary-by-cookie -> canary-weight`
|
`canary-by-header -> canary-by-cookie -> canary-weight`
|
||||||
|
|
||||||
**Note** that when you mark an ingress as canary, then all the other non-canary annotations will be ignored (inherited from the corresponding main ingress) except `nginx.ingress.kubernetes.io/load-balance` and `nginx.ingress.kubernetes.io/upstream-hash-by`.
|
**Note** that when you mark an ingress as canary, then all the other non-canary annotations will be ignored (inherited from the corresponding main ingress) except `nginx.ingress.kubernetes.io/load-balance`, `nginx.ingress.kubernetes.io/upstream-hash-by`, and [annotations related to session affinity](#session-affinity). If you want to restore the original behavior of canaries when session affinity was ignored, set `nginx.ingress.kubernetes.io/affinity-canary-behavior` annotation with value `legacy` on the non-canary ingress definition.
|
||||||
|
|
||||||
**Known Limitations**
|
**Known Limitations**
|
||||||
|
|
||||||
|
@ -163,6 +164,8 @@ The only affinity type available for NGINX is `cookie`.
|
||||||
|
|
||||||
The annotation `nginx.ingress.kubernetes.io/affinity-mode` defines the stickiness of a session. Setting this to `balanced` (default) will redistribute some sessions if a deployment gets scaled up, therefore rebalancing the load on the servers. Setting this to `persistent` will not rebalance sessions to new servers, therefore providing maximum stickiness.
|
The annotation `nginx.ingress.kubernetes.io/affinity-mode` defines the stickiness of a session. Setting this to `balanced` (default) will redistribute some sessions if a deployment gets scaled up, therefore rebalancing the load on the servers. Setting this to `persistent` will not rebalance sessions to new servers, therefore providing maximum stickiness.
|
||||||
|
|
||||||
|
The annotation `nginx.ingress.kubernetes.io/affinity-canary-behavior` defines the behavior of canaries when session affinity is enabled. Setting this to `sticky` (default) will ensure that users that were served by canaries, will continue to be served by canaries. Setting this to `legacy` will restore original canary behavior, when session affinity was ignored.
|
||||||
|
|
||||||
!!! attention
|
!!! attention
|
||||||
If more than one Ingress is defined for a host and at least one Ingress uses `nginx.ingress.kubernetes.io/affinity: cookie`, then only paths on the Ingress using `nginx.ingress.kubernetes.io/affinity` will use session cookie affinity. All paths defined on other Ingresses for the host will be load balanced through the random selection of a backend server.
|
If more than one Ingress is defined for a host and at least one Ingress uses `nginx.ingress.kubernetes.io/affinity: cookie`, then only paths on the Ingress using `nginx.ingress.kubernetes.io/affinity` will use session cookie affinity. All paths defined on other Ingresses for the host will be load balanced through the random selection of a backend server.
|
||||||
|
|
||||||
|
@ -342,7 +345,7 @@ CORS can be controlled with the following annotations:
|
||||||
- Example: `nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"`
|
- Example: `nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"`
|
||||||
|
|
||||||
* `nginx.ingress.kubernetes.io/cors-expose-headers`
|
* `nginx.ingress.kubernetes.io/cors-expose-headers`
|
||||||
controls which headers are exposed to response. This is a multi-valued field, separated by ',' and accepts
|
controls which headers are exposed to response. This is a multi-valued field, separated by ',' and accepts
|
||||||
letters, numbers, _, - and *.
|
letters, numbers, _, - and *.
|
||||||
- Default: *empty*
|
- Default: *empty*
|
||||||
- Example: `nginx.ingress.kubernetes.io/cors-expose-headers: "*, X-CustomResponseHeader"`
|
- Example: `nginx.ingress.kubernetes.io/cors-expose-headers: "*, X-CustomResponseHeader"`
|
||||||
|
|
|
@ -30,19 +30,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
annotationPassthrough = parser.GetAnnotationWithPrefix("ssl-passthrough")
|
annotationPassthrough = parser.GetAnnotationWithPrefix("ssl-passthrough")
|
||||||
annotationAffinityType = parser.GetAnnotationWithPrefix("affinity")
|
annotationAffinityType = parser.GetAnnotationWithPrefix("affinity")
|
||||||
annotationAffinityMode = parser.GetAnnotationWithPrefix("affinity-mode")
|
annotationAffinityMode = parser.GetAnnotationWithPrefix("affinity-mode")
|
||||||
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
|
annotationAffinityCanaryBehavior = parser.GetAnnotationWithPrefix("affinity-canary-behavior")
|
||||||
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
|
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
|
||||||
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
|
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
|
||||||
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
|
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
|
||||||
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
|
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
|
||||||
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
|
||||||
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||||
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
|
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||||
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
|
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
|
||||||
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
|
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
|
||||||
|
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockCfg struct {
|
type mockCfg struct {
|
||||||
|
@ -162,29 +163,38 @@ func TestAffinitySession(t *testing.T) {
|
||||||
ing := buildIngress()
|
ing := buildIngress()
|
||||||
|
|
||||||
fooAnns := []struct {
|
fooAnns := []struct {
|
||||||
annotations map[string]string
|
annotations map[string]string
|
||||||
affinitytype string
|
affinitytype string
|
||||||
affinitymode string
|
affinitymode string
|
||||||
name string
|
cookiename string
|
||||||
|
canarybehavior string
|
||||||
}{
|
}{
|
||||||
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: "route"}, "cookie", "balanced", "route"},
|
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: "route", annotationAffinityCanaryBehavior: ""}, "cookie", "balanced", "route", ""},
|
||||||
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "persistent", annotationAffinityCookieName: "route1"}, "cookie", "persistent", "route1"},
|
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "persistent", annotationAffinityCookieName: "route1", annotationAffinityCanaryBehavior: "sticky"}, "cookie", "persistent", "route1", "sticky"},
|
||||||
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: ""}, "cookie", "balanced", "INGRESSCOOKIE"},
|
{map[string]string{annotationAffinityType: "cookie", annotationAffinityMode: "balanced", annotationAffinityCookieName: "", annotationAffinityCanaryBehavior: "legacy"}, "cookie", "balanced", "INGRESSCOOKIE", "legacy"},
|
||||||
{map[string]string{}, "", "", ""},
|
{map[string]string{}, "", "", "", ""},
|
||||||
{nil, "", "", ""},
|
{nil, "", "", "", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, foo := range fooAnns {
|
for _, foo := range fooAnns {
|
||||||
ing.SetAnnotations(foo.annotations)
|
ing.SetAnnotations(foo.annotations)
|
||||||
r := ec.Extract(ing).SessionAffinity
|
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 {
|
if r.Type != foo.affinitytype {
|
||||||
t.Errorf("Returned %v but expected %v for Name", r.Mode, foo.affinitymode)
|
t.Errorf("Returned %v but expected %v for Type", r.Type, foo.affinitytype)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Cookie.Name != foo.name {
|
if r.Mode != foo.affinitymode {
|
||||||
t.Errorf("Returned %v but expected %v for Name", r.Cookie.Name, foo.name)
|
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 (
|
const (
|
||||||
annotationAffinityType = "affinity"
|
annotationAffinityType = "affinity"
|
||||||
annotationAffinityMode = "affinity-mode"
|
annotationAffinityMode = "affinity-mode"
|
||||||
|
annotationAffinityCanaryBehavior = "affinity-canary-behavior"
|
||||||
|
|
||||||
// If a cookie with this name exists,
|
// If a cookie with this name exists,
|
||||||
// its value is used as an index into the list of available backends.
|
// its value is used as an index into the list of available backends.
|
||||||
annotationAffinityCookieName = "session-cookie-name"
|
annotationAffinityCookieName = "session-cookie-name"
|
||||||
|
@ -66,6 +68,8 @@ type Config struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// The affinity mode, i.e. how sticky a session is
|
// The affinity mode, i.e. how sticky a session is
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
|
// Affinity behavior for canaries (sticky or legacy)
|
||||||
|
CanaryBehavior string `json:"canaryBehavior"`
|
||||||
Cookie
|
Cookie
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +164,11 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||||
am = ""
|
am = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing)
|
||||||
|
if err != nil {
|
||||||
|
cb = ""
|
||||||
|
}
|
||||||
|
|
||||||
switch at {
|
switch at {
|
||||||
case "cookie":
|
case "cookie":
|
||||||
cookie = a.cookieAffinityParse(ing)
|
cookie = a.cookieAffinityParse(ing)
|
||||||
|
@ -169,8 +178,9 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
Type: at,
|
Type: at,
|
||||||
Mode: am,
|
Mode: am,
|
||||||
Cookie: *cookie,
|
CanaryBehavior: cb,
|
||||||
|
Cookie: *cookie,
|
||||||
}, nil
|
}, 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
|
// 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 {
|
if priUps.NoServer {
|
||||||
klog.Warningf("unable to merge alternative backend %v into primary backend %v because %v is a primary backend",
|
klog.Warningf("unable to merge alternative backend %v into primary backend %v because %v is a primary backend",
|
||||||
altUps.Name, priUps.Name, priUps.Name)
|
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 =
|
priUps.AlternativeBackends =
|
||||||
append(priUps.AlternativeBackends, altUps.Name)
|
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",
|
klog.V(2).Infof("matching backend %v found for alternative backend %v",
|
||||||
priUps.Name, altUps.Name)
|
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",
|
klog.V(2).Infof("matching backend %v found for alternative backend %v",
|
||||||
priUps.Name, altUps.Name)
|
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/canary"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/annotations/sessionaffinity"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/controller/config"
|
"k8s.io/ingress-nginx/internal/ingress/controller/config"
|
||||||
ngx_config "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"
|
"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 {
|
for title, tc := range testCases {
|
||||||
|
@ -801,7 +1122,7 @@ func TestMergeAlternativeBackends(t *testing.T) {
|
||||||
if !actualUpstream.Equal(expUpstream) {
|
if !actualUpstream.Equal(expUpstream) {
|
||||||
t.Logf("actual upstream %s alternative backends: %s", actualUpstream.Name, actualUpstream.AlternativeBackends)
|
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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ local function get_implementation(backend)
|
||||||
|
|
||||||
if backend["sessionAffinityConfig"] and
|
if backend["sessionAffinityConfig"] and
|
||||||
backend["sessionAffinityConfig"]["name"] == "cookie" then
|
backend["sessionAffinityConfig"]["name"] == "cookie" then
|
||||||
if backend["sessionAffinityConfig"]["mode"] == 'persistent' then
|
if backend["sessionAffinityConfig"]["mode"] == "persistent" then
|
||||||
name = "sticky_persistent"
|
name = "sticky_persistent"
|
||||||
else
|
else
|
||||||
name = "sticky_balanced"
|
name = "sticky_balanced"
|
||||||
|
@ -186,6 +186,11 @@ local function sync_backends()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function route_to_alternative_balancer(balancer)
|
local function route_to_alternative_balancer(balancer)
|
||||||
|
if balancer.is_affinitized(balancer) then
|
||||||
|
-- If request is already affinitized to a primary balancer, keep the primary balancer.
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
if not balancer.alternative_backends then
|
if not balancer.alternative_backends then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
@ -204,6 +209,13 @@ local function route_to_alternative_balancer(balancer)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if alternative_balancer.is_affinitized(alternative_balancer) then
|
||||||
|
-- If request is affinitized to an alternative balancer, instruct caller to
|
||||||
|
-- switch to alternative.
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Use traffic shaping policy, if request didn't have affinity set.
|
||||||
local traffic_shaping_policy = alternative_balancer.traffic_shaping_policy
|
local traffic_shaping_policy = alternative_balancer.traffic_shaping_policy
|
||||||
if not traffic_shaping_policy then
|
if not traffic_shaping_policy then
|
||||||
ngx.log(ngx.ERR, "traffic shaping policy is not set for balancer ",
|
ngx.log(ngx.ERR, "traffic shaping policy is not set for balancer ",
|
||||||
|
@ -254,6 +266,10 @@ local function route_to_alternative_balancer(balancer)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function get_balancer_by_upstream_name(upstream_name)
|
||||||
|
return balancers[upstream_name]
|
||||||
|
end
|
||||||
|
|
||||||
local function get_balancer()
|
local function get_balancer()
|
||||||
if ngx.ctx.balancer then
|
if ngx.ctx.balancer then
|
||||||
return ngx.ctx.balancer
|
return ngx.ctx.balancer
|
||||||
|
@ -263,7 +279,7 @@ local function get_balancer()
|
||||||
|
|
||||||
local balancer = balancers[backend_name]
|
local balancer = balancers[backend_name]
|
||||||
if not balancer then
|
if not balancer then
|
||||||
return
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if route_to_alternative_balancer(balancer) then
|
if route_to_alternative_balancer(balancer) then
|
||||||
|
@ -352,6 +368,7 @@ setmetatable(_M, {__index = {
|
||||||
sync_backend = sync_backend,
|
sync_backend = sync_backend,
|
||||||
route_to_alternative_balancer = route_to_alternative_balancer,
|
route_to_alternative_balancer = route_to_alternative_balancer,
|
||||||
get_balancer = get_balancer,
|
get_balancer = get_balancer,
|
||||||
|
get_balancer_by_upstream_name = get_balancer_by_upstream_name,
|
||||||
}})
|
}})
|
||||||
|
|
||||||
return _M
|
return _M
|
||||||
|
|
|
@ -68,6 +68,10 @@ function _M.new(self, backend)
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _M.is_affinitized()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function _M.balance(self)
|
function _M.balance(self)
|
||||||
local key = util.generate_var_value(self.hash_by)
|
local key = util.generate_var_value(self.hash_by)
|
||||||
local subset_id = self.instance:find(key)
|
local subset_id = self.instance:find(key)
|
||||||
|
|
|
@ -170,6 +170,10 @@ local function calculate_slow_start_ewma(self)
|
||||||
return total_ewma / endpoints_count
|
return total_ewma / endpoints_count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _M.is_affinitized()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function _M.balance(self)
|
function _M.balance(self)
|
||||||
local peers = self.peers
|
local peers = self.peers
|
||||||
local endpoint, ewma_score = peers[1], -1
|
local endpoint, ewma_score = peers[1], -1
|
||||||
|
|
|
@ -14,6 +14,10 @@ function _M.new(self, o)
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _M.is_affinitized()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function _M.sync(self, backend)
|
function _M.sync(self, backend)
|
||||||
self.traffic_shaping_policy = backend.trafficShapingPolicy
|
self.traffic_shaping_policy = backend.trafficShapingPolicy
|
||||||
self.alternative_backends = backend.alternativeBackends
|
self.alternative_backends = backend.alternativeBackends
|
||||||
|
|
|
@ -13,6 +13,7 @@ local setmetatable = setmetatable
|
||||||
|
|
||||||
local _M = balancer_resty:new()
|
local _M = balancer_resty:new()
|
||||||
local DEFAULT_COOKIE_NAME = "route"
|
local DEFAULT_COOKIE_NAME = "route"
|
||||||
|
local COOKIE_VALUE_DELIMITER = "|"
|
||||||
|
|
||||||
function _M.cookie_name(self)
|
function _M.cookie_name(self)
|
||||||
return self.cookie_session_affinity.name or DEFAULT_COOKIE_NAME
|
return self.cookie_session_affinity.name or DEFAULT_COOKIE_NAME
|
||||||
|
@ -22,7 +23,8 @@ function _M.new(self)
|
||||||
local o = {
|
local o = {
|
||||||
alternative_backends = nil,
|
alternative_backends = nil,
|
||||||
cookie_session_affinity = nil,
|
cookie_session_affinity = nil,
|
||||||
traffic_shaping_policy = nil
|
traffic_shaping_policy = nil,
|
||||||
|
backend_key = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(o, self)
|
setmetatable(o, self)
|
||||||
|
@ -31,13 +33,37 @@ function _M.new(self)
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
|
||||||
function _M.get_cookie(self)
|
function _M.get_cookie_parsed(self)
|
||||||
local cookie, err = ck:new()
|
local cookie, err = ck:new()
|
||||||
if not cookie then
|
if not cookie then
|
||||||
ngx.log(ngx.ERR, err)
|
ngx.log(ngx.ERR, err)
|
||||||
end
|
end
|
||||||
|
|
||||||
return cookie:get(self:cookie_name())
|
local result = {
|
||||||
|
upstream_key = nil,
|
||||||
|
backend_key = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
local raw_value = cookie:get(self:cookie_name())
|
||||||
|
if not raw_value then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local parsed_value, len = split.split_string(raw_value, COOKIE_VALUE_DELIMITER)
|
||||||
|
if len == 0 then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
result.upstream_key = parsed_value[1]
|
||||||
|
if len > 1 then
|
||||||
|
result.backend_key = parsed_value[2]
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function _M.get_cookie(self)
|
||||||
|
return self:get_cookie_parsed().upstream_key
|
||||||
end
|
end
|
||||||
|
|
||||||
function _M.set_cookie(self, value)
|
function _M.set_cookie(self, value)
|
||||||
|
@ -63,7 +89,7 @@ function _M.set_cookie(self, value)
|
||||||
|
|
||||||
local cookie_data = {
|
local cookie_data = {
|
||||||
key = self:cookie_name(),
|
key = self:cookie_name(),
|
||||||
value = value,
|
value = value .. COOKIE_VALUE_DELIMITER .. self.backend_key,
|
||||||
path = cookie_path,
|
path = cookie_path,
|
||||||
httponly = true,
|
httponly = true,
|
||||||
samesite = cookie_samesite,
|
samesite = cookie_samesite,
|
||||||
|
@ -86,6 +112,10 @@ function _M.set_cookie(self, value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _M.is_affinitized(self)
|
||||||
|
return self:get_cookie_parsed().backend_key == self.backend_key
|
||||||
|
end
|
||||||
|
|
||||||
function _M.get_last_failure()
|
function _M.get_last_failure()
|
||||||
return ngx_balancer.get_last_failure()
|
return ngx_balancer.get_last_failure()
|
||||||
end
|
end
|
||||||
|
@ -166,6 +196,7 @@ function _M.sync(self, backend)
|
||||||
self.traffic_shaping_policy = backend.trafficShapingPolicy
|
self.traffic_shaping_policy = backend.trafficShapingPolicy
|
||||||
self.alternative_backends = backend.alternativeBackends
|
self.alternative_backends = backend.alternativeBackends
|
||||||
self.cookie_session_affinity = backend.sessionAffinityConfig.cookieSessionAffinity
|
self.cookie_session_affinity = backend.sessionAffinityConfig.cookieSessionAffinity
|
||||||
|
self.backend_key = ngx.md5(ngx.md5(backend.name) .. backend.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
return _M
|
return _M
|
||||||
|
|
|
@ -5,24 +5,35 @@ local util = require("util")
|
||||||
|
|
||||||
local original_ngx = ngx
|
local original_ngx = ngx
|
||||||
|
|
||||||
function mock_ngx(mock)
|
|
||||||
local _ngx = mock
|
|
||||||
setmetatable(_ngx, {__index = _G.ngx})
|
|
||||||
_G.ngx = _ngx
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reset_ngx()
|
|
||||||
_G.ngx = original_ngx
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reset_sticky_balancer()
|
local function reset_sticky_balancer()
|
||||||
package.loaded["balancer.sticky"] = nil
|
package.loaded["balancer.sticky"] = nil
|
||||||
package.loaded["balancer.sticky_balanced"] = nil
|
package.loaded["balancer.sticky_balanced"] = nil
|
||||||
package.loaded["balancer.sticky_persistent"] = nil
|
package.loaded["balancer.sticky_persistent"] = nil
|
||||||
|
|
||||||
sticky_balanced = require("balancer.sticky_balanced")
|
sticky_balanced = require("balancer.sticky_balanced")
|
||||||
sticky_persistent = require("balancer.sticky_persistent")
|
sticky_persistent = require("balancer.sticky_persistent")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function mock_ngx(mock, after_mock_set)
|
||||||
|
local _ngx = mock
|
||||||
|
setmetatable(_ngx, { __index = ngx })
|
||||||
|
_G.ngx = _ngx
|
||||||
|
|
||||||
|
if after_mock_set then
|
||||||
|
after_mock_set()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Balancer module caches ngx module, must be reset after mocks were configured.
|
||||||
|
reset_sticky_balancer()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reset_ngx()
|
||||||
|
_G.ngx = original_ngx
|
||||||
|
|
||||||
|
-- Ensure balancer cache is reset.
|
||||||
|
_G.ngx.ctx.balancer = nil
|
||||||
|
end
|
||||||
|
|
||||||
function get_mocked_cookie_new()
|
function get_mocked_cookie_new()
|
||||||
local o = { value = nil }
|
local o = { value = nil }
|
||||||
local mock = {
|
local mock = {
|
||||||
|
@ -55,7 +66,6 @@ end
|
||||||
describe("Sticky", function()
|
describe("Sticky", function()
|
||||||
before_each(function()
|
before_each(function()
|
||||||
mock_ngx({ var = { location_path = "/", host = "test.com" } })
|
mock_ngx({ var = { location_path = "/", host = "test.com" } })
|
||||||
reset_sticky_balancer()
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
after_each(function()
|
after_each(function()
|
||||||
|
@ -65,29 +75,44 @@ describe("Sticky", function()
|
||||||
local test_backend = get_test_backend()
|
local test_backend = get_test_backend()
|
||||||
local test_backend_endpoint= test_backend.endpoints[1].address .. ":" .. test_backend.endpoints[1].port
|
local test_backend_endpoint= test_backend.endpoints[1].address .. ":" .. test_backend.endpoints[1].port
|
||||||
|
|
||||||
|
local legacy_cookie_value = test_backend_endpoint
|
||||||
|
local function create_current_cookie_value(backend_key)
|
||||||
|
return test_backend_endpoint .. "|" .. backend_key
|
||||||
|
end
|
||||||
|
|
||||||
describe("new(backend)", function()
|
describe("new(backend)", function()
|
||||||
context("when backend specifies cookie name", function()
|
describe("when backend specifies cookie name", function()
|
||||||
local function test(sticky)
|
local function test_with(sticky_balancer_type)
|
||||||
local sticky_balancer_instance = sticky:new(test_backend)
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
local test_backend_cookie_name = test_backend.sessionAffinityConfig.cookieSessionAffinity.name
|
local test_backend_cookie_name = test_backend.sessionAffinityConfig.cookieSessionAffinity.name
|
||||||
assert.equal(sticky_balancer_instance:cookie_name(), test_backend_cookie_name)
|
assert.equal(sticky_balancer_instance:cookie_name(), test_backend_cookie_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it("returns an instance containing the corresponding cookie name", function() test(sticky_balanced) end)
|
it("returns an instance containing the corresponding cookie name", function() test_with(sticky_balanced) end)
|
||||||
it("returns an instance containing the corresponding cookie name", function() test(sticky_persistent) end)
|
it("returns an instance containing the corresponding cookie name", function() test_with(sticky_persistent) end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when backend does not specify cookie name", function()
|
describe("when backend does not specify cookie name", function()
|
||||||
local function test(sticky)
|
local function test_with(sticky_balancer_type)
|
||||||
local temp_backend = util.deepcopy(test_backend)
|
local temp_backend = util.deepcopy(test_backend)
|
||||||
temp_backend.sessionAffinityConfig.cookieSessionAffinity.name = nil
|
temp_backend.sessionAffinityConfig.cookieSessionAffinity.name = nil
|
||||||
local sticky_balancer_instance = sticky:new(temp_backend)
|
local sticky_balancer_instance = sticky_balancer_type:new(temp_backend)
|
||||||
local default_cookie_name = "route"
|
local default_cookie_name = "route"
|
||||||
assert.equal(sticky_balancer_instance:cookie_name(), default_cookie_name)
|
assert.equal(sticky_balancer_instance:cookie_name(), default_cookie_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it("returns an instance with 'route' as cookie name", function() test(sticky_balanced) end)
|
it("returns an instance with 'route' as cookie name", function() test_with(sticky_balanced) end)
|
||||||
it("returns an instance with 'route' as cookie name", function() test(sticky_persistent) end)
|
it("returns an instance with 'route' as cookie name", function() test_with(sticky_persistent) end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("backend_key", function()
|
||||||
|
local function test_with(sticky_balancer_type)
|
||||||
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
|
assert.is_truthy(sticky_balancer_instance.backend_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it("calculates at construction time", function() test_with(sticky_balanced) end)
|
||||||
|
it("calculates at construction time", function() test_with(sticky_persistent) end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -95,28 +120,25 @@ describe("Sticky", function()
|
||||||
local mocked_cookie_new = cookie.new
|
local mocked_cookie_new = cookie.new
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
package.loaded["balancer.sticky_balanced"] = nil
|
reset_sticky_balancer()
|
||||||
package.loaded["balancer.sticky_persistent"] = nil
|
|
||||||
sticky_balanced = require("balancer.sticky_balanced")
|
|
||||||
sticky_persistent = require("balancer.sticky_persistent")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
after_each(function()
|
after_each(function()
|
||||||
cookie.new = mocked_cookie_new
|
cookie.new = mocked_cookie_new
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when client doesn't have a cookie set and location is in cookie_locations", function()
|
describe("when client doesn't have a cookie set and location is in cookie_locations", function()
|
||||||
|
|
||||||
local function test_pick_endpoint(sticky)
|
local function test_pick_endpoint_with(sticky_balancer_type)
|
||||||
local sticky_balancer_instance = sticky:new(test_backend)
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
local peer = sticky_balancer_instance:balance()
|
local peer = sticky_balancer_instance:balance()
|
||||||
assert.equal(test_backend_endpoint, peer)
|
assert.equal(test_backend_endpoint, peer)
|
||||||
end
|
end
|
||||||
|
|
||||||
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_balanced) end)
|
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_balanced) end)
|
||||||
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_persistent) end)
|
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_persistent) end)
|
||||||
|
|
||||||
local function test_set_cookie(sticky)
|
local function test_set_cookie_with(sticky_balancer_type)
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
local cookie_instance = {
|
local cookie_instance = {
|
||||||
|
@ -137,15 +159,15 @@ describe("Sticky", function()
|
||||||
local b = get_test_backend()
|
local b = get_test_backend()
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
|
||||||
local sticky_balancer_instance = sticky:new(b)
|
local sticky_balancer_instance = sticky_balancer_type:new(b)
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_called()
|
assert.spy(s).was_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("sets a cookie on the client", function() test_set_cookie(sticky_balanced) end)
|
it("sets a cookie on the client", function() test_set_cookie_with(sticky_balanced) end)
|
||||||
it("sets a cookie on the client", function() test_set_cookie(sticky_persistent) end)
|
it("sets a cookie on the client", function() test_set_cookie_with(sticky_persistent) end)
|
||||||
|
|
||||||
local function test_set_ssl_cookie(sticky)
|
local function test_set_ssl_cookie_with(sticky_balancer_type)
|
||||||
ngx.var.https = "on"
|
ngx.var.https = "on"
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
|
@ -167,21 +189,17 @@ describe("Sticky", function()
|
||||||
local b = get_test_backend()
|
local b = get_test_backend()
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
|
||||||
local sticky_balancer_instance = sticky:new(b)
|
local sticky_balancer_instance = sticky_balancer_type:new(b)
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_called()
|
assert.spy(s).was_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("sets a secure cookie on the client when being in ssl mode", function()
|
it("sets a secure cookie on the client when being in ssl mode", function() test_set_ssl_cookie_with(sticky_balanced) end)
|
||||||
test_set_ssl_cookie(sticky_balanced)
|
it("sets a secure cookie on the client when being in ssl mode", function() test_set_ssl_cookie_with(sticky_persistent) end)
|
||||||
end)
|
|
||||||
it("sets a secure cookie on the client when being in ssl mode", function()
|
|
||||||
test_set_ssl_cookie(sticky_persistent)
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when client doesn't have a cookie set and cookie_locations contains a matching wildcard location",
|
describe("when client doesn't have a cookie set and cookie_locations contains a matching wildcard location", function()
|
||||||
function()
|
|
||||||
before_each(function ()
|
before_each(function ()
|
||||||
ngx.var.host = "dev.test.com"
|
ngx.var.host = "dev.test.com"
|
||||||
end)
|
end)
|
||||||
|
@ -189,7 +207,7 @@ describe("Sticky", function()
|
||||||
ngx.var.host = "test.com"
|
ngx.var.host = "test.com"
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function test(sticky)
|
local function test_with(sticky_balancer_type)
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
local cookie_instance = {
|
local cookie_instance = {
|
||||||
|
@ -211,27 +229,27 @@ describe("Sticky", function()
|
||||||
local b = get_test_backend()
|
local b = get_test_backend()
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations["*.test.com"] = {"/"}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations["*.test.com"] = {"/"}
|
||||||
local sticky_balancer_instance = sticky:new(b)
|
local sticky_balancer_instance = sticky_balancer_type:new(b)
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_called()
|
assert.spy(s).was_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("sets a cookie on the client", function() test(sticky_balanced) end)
|
it("sets a cookie on the client", function() test_with(sticky_balanced) end)
|
||||||
it("sets a cookie on the client", function() test(sticky_persistent) end)
|
it("sets a cookie on the client", function() test_with(sticky_persistent) end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when client doesn't have a cookie set and location not in cookie_locations", function()
|
describe("when client doesn't have a cookie set and location not in cookie_locations", function()
|
||||||
|
|
||||||
local function test_pick_endpoint(sticky)
|
local function test_pick_endpoint_with(sticky_balancer_type)
|
||||||
local sticky_balancer_instance = sticky:new(test_backend)
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
local peer = sticky_balancer_instance:balance()
|
local peer = sticky_balancer_instance:balance()
|
||||||
assert.equal(peer, test_backend_endpoint)
|
assert.equal(peer, test_backend_endpoint)
|
||||||
end
|
end
|
||||||
|
|
||||||
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_balanced) end)
|
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_balanced) end)
|
||||||
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_persistent) end)
|
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_persistent) end)
|
||||||
|
|
||||||
local function test_no_cookie(sticky)
|
local function test_no_cookie_with(sticky_balancer_type)
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
local cookie_instance = {
|
local cookie_instance = {
|
||||||
|
@ -248,34 +266,34 @@ describe("Sticky", function()
|
||||||
s = spy.on(cookie_instance, "set")
|
s = spy.on(cookie_instance, "set")
|
||||||
return cookie_instance, false
|
return cookie_instance, false
|
||||||
end
|
end
|
||||||
local sticky_balancer_instance = sticky:new(get_test_backend())
|
local sticky_balancer_instance = sticky_balancer_type:new(get_test_backend())
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_not_called()
|
assert.spy(s).was_not_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("does not set a cookie on the client", function() test_no_cookie(sticky_balanced) end)
|
it("does not set a cookie on the client", function() test_no_cookie_with(sticky_balanced) end)
|
||||||
it("does not set a cookie on the client", function() test_no_cookie(sticky_persistent) end)
|
it("does not set a cookie on the client", function() test_no_cookie_with(sticky_persistent) end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when client has a cookie set", function()
|
describe("when client has a cookie set", function()
|
||||||
|
|
||||||
local function test_no_cookie(sticky)
|
local function test_no_cookie_with(sticky_balancer_type)
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
local return_obj = {
|
local return_obj = {
|
||||||
set = function(v) return false, nil end,
|
set = function(v) return false, nil end,
|
||||||
get = function(k) return test_backend_endpoint end,
|
get = function(k) return legacy_cookie_value end,
|
||||||
}
|
}
|
||||||
s = spy.on(return_obj, "set")
|
s = spy.on(return_obj, "set")
|
||||||
return return_obj, false
|
return return_obj, false
|
||||||
end
|
end
|
||||||
local sticky_balancer_instance = sticky:new(test_backend)
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_not_called()
|
assert.spy(s).was_not_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("does not set a cookie", function() test_no_cookie(sticky_balanced) end)
|
it("does not set a cookie", function() test_no_cookie_with(sticky_balanced) end)
|
||||||
it("does not set a cookie", function() test_no_cookie(sticky_persistent) end)
|
it("does not set a cookie", function() test_no_cookie_with(sticky_persistent) end)
|
||||||
|
|
||||||
local function test_correct_endpoint(sticky)
|
local function test_correct_endpoint(sticky)
|
||||||
local sticky_balancer_instance = sticky:new(test_backend)
|
local sticky_balancer_instance = sticky:new(test_backend)
|
||||||
|
@ -312,17 +330,16 @@ describe("Sticky", function()
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
mock_ngx({ var = { location_path = "/", host = "test.com" } })
|
mock_ngx({ var = { location_path = "/", host = "test.com" } })
|
||||||
reset_sticky_balancer()
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
after_each(function()
|
after_each(function()
|
||||||
reset_ngx()
|
reset_ngx()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when request to upstream fails", function()
|
describe("when request to upstream fails", function()
|
||||||
|
|
||||||
local function test(sticky, change_on_failure)
|
local function test_with(sticky_balancer_type, change_on_failure)
|
||||||
local sticky_balancer_instance = sticky:new(get_several_test_backends(change_on_failure))
|
local sticky_balancer_instance = sticky_balancer_type:new(get_several_test_backends(change_on_failure))
|
||||||
|
|
||||||
local old_upstream = sticky_balancer_instance:balance()
|
local old_upstream = sticky_balancer_instance:balance()
|
||||||
assert.is.Not.Nil(old_upstream)
|
assert.is.Not.Nil(old_upstream)
|
||||||
|
@ -349,29 +366,21 @@ describe("Sticky", function()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it("changes upstream when change_on_failure option is true", function()
|
it("changes upstream when change_on_failure option is true", function() test_with(sticky_balanced, true) end)
|
||||||
test(sticky_balanced, true)
|
it("changes upstream when change_on_failure option is true", function() test_with(sticky_persistent, true) end)
|
||||||
end)
|
|
||||||
it("changes upstream when change_on_failure option is true", function()
|
it("changes upstream when change_on_failure option is false", function() test_with(sticky_balanced, false) end)
|
||||||
test(sticky_balanced, false)
|
it("changes upstream when change_on_failure option is false", function() test_with(sticky_persistent, false) end)
|
||||||
end)
|
|
||||||
it("changes upstream when change_on_failure option is true", function()
|
|
||||||
test(sticky_persistent, true)
|
|
||||||
end)
|
|
||||||
it("changes upstream when change_on_failure option is true", function()
|
|
||||||
test(sticky_persistent, false)
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("when client doesn't have a cookie set and no host header, matching default server '_'",
|
describe("when client doesn't have a cookie set and no host header, matching default server '_'", function()
|
||||||
function()
|
|
||||||
before_each(function ()
|
before_each(function ()
|
||||||
ngx.var.host = "not-default-server"
|
ngx.var.host = "not-default-server"
|
||||||
ngx.var.server_name = "_"
|
ngx.var.server_name = "_"
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function test(sticky)
|
local function test_with(sticky_balancer_type)
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
local cookie_instance = {
|
local cookie_instance = {
|
||||||
|
@ -393,30 +402,27 @@ describe("Sticky", function()
|
||||||
local b = get_test_backend()
|
local b = get_test_backend()
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations["_"] = {"/"}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations["_"] = {"/"}
|
||||||
local sticky_balancer_instance = sticky:new(b)
|
local sticky_balancer_instance = sticky_balancer_type:new(b)
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_called()
|
assert.spy(s).was_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("sets a cookie on the client", function() test(sticky_balanced) end)
|
it("sets a cookie on the client", function() test_with(sticky_balanced) end)
|
||||||
it("sets a cookie on the client", function() test(sticky_persistent) end)
|
it("sets a cookie on the client", function() test_with(sticky_persistent) end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("SameSite settings", function()
|
describe("SameSite settings", function()
|
||||||
local mocked_cookie_new = cookie.new
|
local mocked_cookie_new = cookie.new
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
package.loaded["balancer.sticky_balanced"] = nil
|
reset_sticky_balancer()
|
||||||
package.loaded["balancer.sticky_persistent"] = nil
|
|
||||||
sticky_balanced = require("balancer.sticky_balanced")
|
|
||||||
sticky_persistent = require("balancer.sticky_persistent")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
after_each(function()
|
after_each(function()
|
||||||
cookie.new = mocked_cookie_new
|
cookie.new = mocked_cookie_new
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function test_set_cookie(sticky, samesite, conditional_samesite_none, expected_path, expected_samesite)
|
local function test_set_cookie_with(sticky_balancer_type, samesite, conditional_samesite_none, expected_path, expected_samesite)
|
||||||
local s = {}
|
local s = {}
|
||||||
cookie.new = function(self)
|
cookie.new = function(self)
|
||||||
local cookie_instance = {
|
local cookie_instance = {
|
||||||
|
@ -439,34 +445,179 @@ describe("Sticky", function()
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
|
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.samesite = samesite
|
b.sessionAffinityConfig.cookieSessionAffinity.samesite = samesite
|
||||||
b.sessionAffinityConfig.cookieSessionAffinity.conditional_samesite_none = conditional_samesite_none
|
b.sessionAffinityConfig.cookieSessionAffinity.conditional_samesite_none = conditional_samesite_none
|
||||||
local sticky_balancer_instance = sticky:new(b)
|
local sticky_balancer_instance = sticky_balancer_type:new(b)
|
||||||
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
|
||||||
assert.spy(s).was_called()
|
assert.spy(s).was_called()
|
||||||
end
|
end
|
||||||
|
|
||||||
it("returns a cookie with SameSite=Strict when user specifies samesite strict", function()
|
it("returns a cookie with SameSite=Strict when user specifies samesite strict", function()
|
||||||
test_set_cookie(sticky_balanced, "Strict", false, "/", "Strict")
|
test_set_cookie_with(sticky_balanced, "Strict", false, "/", "Strict")
|
||||||
end)
|
end)
|
||||||
it("returns a cookie with SameSite=Strict when user specifies samesite strict and conditional samesite none", function()
|
it("returns a cookie with SameSite=Strict when user specifies samesite strict and conditional samesite none", function()
|
||||||
test_set_cookie(sticky_balanced, "Strict", true, "/", "Strict")
|
test_set_cookie_with(sticky_balanced, "Strict", true, "/", "Strict")
|
||||||
end)
|
end)
|
||||||
it("returns a cookie with SameSite=Lax when user specifies samesite lax", function()
|
it("returns a cookie with SameSite=Lax when user specifies samesite lax", function()
|
||||||
test_set_cookie(sticky_balanced, "Lax", false, "/", "Lax")
|
test_set_cookie_with(sticky_balanced, "Lax", false, "/", "Lax")
|
||||||
end)
|
end)
|
||||||
it("returns a cookie with SameSite=Lax when user specifies samesite lax and conditional samesite none", function()
|
it("returns a cookie with SameSite=Lax when user specifies samesite lax and conditional samesite none", function()
|
||||||
test_set_cookie(sticky_balanced, "Lax", true, "/", "Lax")
|
test_set_cookie_with(sticky_balanced, "Lax", true, "/", "Lax")
|
||||||
end)
|
end)
|
||||||
it("returns a cookie with SameSite=None when user specifies samesite None", function()
|
it("returns a cookie with SameSite=None when user specifies samesite None", function()
|
||||||
test_set_cookie(sticky_balanced, "None", false, "/", "None")
|
test_set_cookie_with(sticky_balanced, "None", false, "/", "None")
|
||||||
end)
|
end)
|
||||||
it("returns a cookie with SameSite=None when user specifies samesite None and conditional samesite none with supported user agent", function()
|
it("returns a cookie with SameSite=None when user specifies samesite None and conditional samesite none with supported user agent", function()
|
||||||
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.2704.103 Safari/537.36"} })
|
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.2704.103 Safari/537.36"} })
|
||||||
test_set_cookie(sticky_balanced, "None", true, "/", "None")
|
test_set_cookie_with(sticky_balanced, "None", true, "/", "None")
|
||||||
end)
|
end)
|
||||||
it("returns a cookie without SameSite=None when user specifies samesite None and conditional samesite none with unsupported user agent", function()
|
it("returns a cookie without SameSite=None when user specifies samesite None and conditional samesite none with unsupported user agent", function()
|
||||||
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"} })
|
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"} })
|
||||||
reset_sticky_balancer()
|
test_set_cookie_with(sticky_balanced, "None", true, "/", nil)
|
||||||
test_set_cookie(sticky_balanced, "None", true, "/", nil)
|
end)
|
||||||
|
|
||||||
|
it("returns a cookie with SameSite=Strict when user specifies samesite strict", function()
|
||||||
|
test_set_cookie_with(sticky_persistent, "Strict", false, "/", "Strict")
|
||||||
|
end)
|
||||||
|
it("returns a cookie with SameSite=Strict when user specifies samesite strict and conditional samesite none", function()
|
||||||
|
test_set_cookie_with(sticky_persistent, "Strict", true, "/", "Strict")
|
||||||
|
end)
|
||||||
|
it("returns a cookie with SameSite=Lax when user specifies samesite lax", function()
|
||||||
|
test_set_cookie_with(sticky_persistent, "Lax", false, "/", "Lax")
|
||||||
|
end)
|
||||||
|
it("returns a cookie with SameSite=Lax when user specifies samesite lax and conditional samesite none", function()
|
||||||
|
test_set_cookie_with(sticky_persistent, "Lax", true, "/", "Lax")
|
||||||
|
end)
|
||||||
|
it("returns a cookie with SameSite=None when user specifies samesite None", function()
|
||||||
|
test_set_cookie_with(sticky_persistent, "None", false, "/", "None")
|
||||||
|
end)
|
||||||
|
it("returns a cookie with SameSite=None when user specifies samesite None and conditional samesite none with supported user agent", function()
|
||||||
|
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.2704.103 Safari/537.36"} })
|
||||||
|
test_set_cookie_with(sticky_persistent, "None", true, "/", "None")
|
||||||
|
end)
|
||||||
|
it("returns a cookie without SameSite=None when user specifies samesite None and conditional samesite none with unsupported user agent", function()
|
||||||
|
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"} })
|
||||||
|
test_set_cookie_with(sticky_persistent, "None", true, "/", nil)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("get_cookie()", function()
|
||||||
|
|
||||||
|
describe("legacy cookie value", function()
|
||||||
|
local function test_with(sticky_balancer_type)
|
||||||
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
|
|
||||||
|
cookie.new = function(self)
|
||||||
|
local return_obj = {
|
||||||
|
set = function(v) return false, nil end,
|
||||||
|
get = function(k) return legacy_cookie_value end,
|
||||||
|
}
|
||||||
|
return return_obj, false
|
||||||
|
end
|
||||||
|
|
||||||
|
assert.equal(test_backend_endpoint, sticky_balancer_instance.get_cookie(sticky_balancer_instance))
|
||||||
|
end
|
||||||
|
|
||||||
|
it("retrieves upstream key value", function() test_with(sticky_balanced) end)
|
||||||
|
it("retrieves upstream key value", function() test_with(sticky_persistent) end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("current cookie value", function()
|
||||||
|
local function test_with(sticky_balancer_type)
|
||||||
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
|
|
||||||
|
cookie.new = function(self)
|
||||||
|
local return_obj = {
|
||||||
|
set = function(v) return false, nil end,
|
||||||
|
get = function(k) return create_current_cookie_value(sticky_balancer_instance.backend_key) end,
|
||||||
|
}
|
||||||
|
return return_obj, false
|
||||||
|
end
|
||||||
|
|
||||||
|
assert.equal(test_backend_endpoint, sticky_balancer_instance.get_cookie(sticky_balancer_instance))
|
||||||
|
end
|
||||||
|
|
||||||
|
it("retrieves upstream key value", function() test_with(sticky_balanced) end)
|
||||||
|
it("retrieves upstream key value", function() test_with(sticky_persistent) end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("get_cookie_parsed()", function()
|
||||||
|
|
||||||
|
describe("legacy cookie value", function()
|
||||||
|
local function test_with(sticky_balancer_type)
|
||||||
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
|
|
||||||
|
cookie.new = function(self)
|
||||||
|
local return_obj = {
|
||||||
|
set = function(v) return false, nil end,
|
||||||
|
get = function(k) return legacy_cookie_value end,
|
||||||
|
}
|
||||||
|
return return_obj, false
|
||||||
|
end
|
||||||
|
|
||||||
|
local parsed_cookie = sticky_balancer_instance.get_cookie_parsed(sticky_balancer_instance)
|
||||||
|
|
||||||
|
assert.is_truthy(parsed_cookie)
|
||||||
|
assert.equal(test_backend_endpoint, parsed_cookie.upstream_key)
|
||||||
|
assert.is_falsy(parsed_cookie.backend_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it("retrieves upstream key value", function() test_with(sticky_balanced) end)
|
||||||
|
it("retrieves upstream key value", function() test_with(sticky_persistent) end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("current cookie value", function()
|
||||||
|
local function test_with(sticky_balancer_type)
|
||||||
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
|
|
||||||
|
cookie.new = function(self)
|
||||||
|
local return_obj = {
|
||||||
|
set = function(v) return false, nil end,
|
||||||
|
get = function(k) return create_current_cookie_value(sticky_balancer_instance.backend_key) end,
|
||||||
|
}
|
||||||
|
return return_obj, false
|
||||||
|
end
|
||||||
|
|
||||||
|
local parsed_cookie = sticky_balancer_instance.get_cookie_parsed(sticky_balancer_instance)
|
||||||
|
|
||||||
|
assert.is_truthy(parsed_cookie)
|
||||||
|
assert.equal(test_backend_endpoint, parsed_cookie.upstream_key)
|
||||||
|
assert.equal(sticky_balancer_instance.backend_key, parsed_cookie.backend_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it("retrieves all supported values", function() test_with(sticky_balanced) end)
|
||||||
|
it("retrieves all supported values", function() test_with(sticky_persistent) end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("set_cookie()", function()
|
||||||
|
|
||||||
|
local function test_with(sticky_balancer_type)
|
||||||
|
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
|
||||||
|
|
||||||
|
local cookieSetSpy = {}
|
||||||
|
cookie.new = function(self)
|
||||||
|
local return_obj = {
|
||||||
|
set = function(self, payload)
|
||||||
|
assert.equal(create_current_cookie_value(sticky_balancer_instance.backend_key), payload.value)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
end,
|
||||||
|
get = function(k) return nil end,
|
||||||
|
}
|
||||||
|
cookieSetSpy = spy.on(return_obj, "set")
|
||||||
|
|
||||||
|
return return_obj, false
|
||||||
|
end
|
||||||
|
|
||||||
|
sticky_balancer_instance.set_cookie(sticky_balancer_instance, test_backend_endpoint)
|
||||||
|
|
||||||
|
assert.spy(cookieSetSpy).was_called()
|
||||||
|
end
|
||||||
|
|
||||||
|
it("constructs correct cookie value", function() test_with(sticky_balanced) end)
|
||||||
|
it("constructs correct cookie value", function() test_with(sticky_persistent) end)
|
||||||
|
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -6,12 +6,9 @@ local original_ngx = ngx
|
||||||
|
|
||||||
local function reset_ngx()
|
local function reset_ngx()
|
||||||
_G.ngx = original_ngx
|
_G.ngx = original_ngx
|
||||||
end
|
|
||||||
|
|
||||||
local function mock_ngx(mock)
|
-- Ensure balancer cache is reset.
|
||||||
local _ngx = mock
|
_G.ngx.ctx.balancer = nil
|
||||||
setmetatable(_ngx, { __index = ngx })
|
|
||||||
_G.ngx = _ngx
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function reset_balancer()
|
local function reset_balancer()
|
||||||
|
@ -19,6 +16,19 @@ local function reset_balancer()
|
||||||
balancer = require("balancer")
|
balancer = require("balancer")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function mock_ngx(mock, after_mock_set)
|
||||||
|
local _ngx = mock
|
||||||
|
setmetatable(_ngx, { __index = ngx })
|
||||||
|
_G.ngx = _ngx
|
||||||
|
|
||||||
|
if after_mock_set then
|
||||||
|
after_mock_set()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Balancer module caches ngx module, must be reset after mocks were configured.
|
||||||
|
reset_balancer()
|
||||||
|
end
|
||||||
|
|
||||||
local function reset_expected_implementations()
|
local function reset_expected_implementations()
|
||||||
expected_implementations = {
|
expected_implementations = {
|
||||||
["access-router-production-web-80"] = package.loaded["balancer.round_robin"],
|
["access-router-production-web-80"] = package.loaded["balancer.round_robin"],
|
||||||
|
@ -26,7 +36,8 @@ local function reset_expected_implementations()
|
||||||
["my-dummy-app-2"] = package.loaded["balancer.chash"],
|
["my-dummy-app-2"] = package.loaded["balancer.chash"],
|
||||||
["my-dummy-app-3"] = package.loaded["balancer.sticky_persistent"],
|
["my-dummy-app-3"] = package.loaded["balancer.sticky_persistent"],
|
||||||
["my-dummy-app-4"] = package.loaded["balancer.ewma"],
|
["my-dummy-app-4"] = package.loaded["balancer.ewma"],
|
||||||
["my-dummy-app-5"] = package.loaded["balancer.sticky_balanced"]
|
["my-dummy-app-5"] = package.loaded["balancer.sticky_balanced"],
|
||||||
|
["my-dummy-app-6"] = package.loaded["balancer.chashsubset"]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,20 +59,35 @@ local function reset_backends()
|
||||||
cookie = ""
|
cookie = ""
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ name = "my-dummy-app-1", ["load-balance"] = "round_robin", },
|
|
||||||
{
|
{
|
||||||
name = "my-dummy-app-2", ["load-balance"] = "chash",
|
name = "my-dummy-app-1",
|
||||||
|
["load-balance"] = "round_robin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "my-dummy-app-2",
|
||||||
|
["load-balance"] = "round_robin", -- upstreamHashByConfig will take priority.
|
||||||
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
|
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "my-dummy-app-3", ["load-balance"] = "ewma",
|
name = "my-dummy-app-3",
|
||||||
sessionAffinityConfig = { name = "cookie", mode = 'persistent', cookieSessionAffinity = { name = "route" } }
|
["load-balance"] = "ewma", -- sessionAffinityConfig will take priority.
|
||||||
|
sessionAffinityConfig = { name = "cookie", mode = "persistent", cookieSessionAffinity = { name = "route" } }
|
||||||
},
|
},
|
||||||
{ name = "my-dummy-app-4", ["load-balance"] = "ewma", },
|
|
||||||
{
|
{
|
||||||
name = "my-dummy-app-5", ["load-balance"] = "ewma", ["upstream-hash-by"] = "$request_uri",
|
name = "my-dummy-app-4",
|
||||||
|
["load-balance"] = "ewma",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "my-dummy-app-5",
|
||||||
|
["load-balance"] = "ewma", -- sessionAffinityConfig will take priority.
|
||||||
|
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
|
||||||
sessionAffinityConfig = { name = "cookie", cookieSessionAffinity = { name = "route" } }
|
sessionAffinityConfig = { name = "cookie", cookieSessionAffinity = { name = "route" } }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name = "my-dummy-app-6",
|
||||||
|
["load-balance"] = "ewma", -- upstreamHashByConfig will take priority.
|
||||||
|
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", ["upstream-hash-by-subset"] = "true", }
|
||||||
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -77,7 +103,7 @@ describe("Balancer", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("get_implementation()", function()
|
describe("get_implementation()", function()
|
||||||
it("returns correct implementation for given backend", function()
|
it("uses heuristics to select correct load balancer implementation for a given backend", function()
|
||||||
for _, backend in pairs(backends) do
|
for _, backend in pairs(backends) do
|
||||||
local expected_implementation = expected_implementations[backend.name]
|
local expected_implementation = expected_implementations[backend.name]
|
||||||
local implementation = balancer.get_implementation(backend)
|
local implementation = balancer.get_implementation(backend)
|
||||||
|
@ -89,8 +115,8 @@ describe("Balancer", function()
|
||||||
describe("get_balancer()", function()
|
describe("get_balancer()", function()
|
||||||
it("always returns the same balancer for given request context", function()
|
it("always returns the same balancer for given request context", function()
|
||||||
local backend = {
|
local backend = {
|
||||||
name = "my-dummy-app-6", ["load-balance"] = "ewma",
|
name = "my-dummy-app-100", ["load-balance"] = "ewma",
|
||||||
alternativeBackends = { "my-dummy-canary-app-6" },
|
alternativeBackends = { "my-dummy-canary-app-100" },
|
||||||
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
|
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
|
||||||
trafficShapingPolicy = {
|
trafficShapingPolicy = {
|
||||||
weight = 0,
|
weight = 0,
|
||||||
|
@ -100,8 +126,8 @@ describe("Balancer", function()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
local canary_backend = {
|
local canary_backend = {
|
||||||
name = "my-dummy-canary-app-6", ["load-balance"] = "ewma",
|
name = "my-dummy-canary-app-100", ["load-balance"] = "ewma",
|
||||||
alternativeBackends = { "my-dummy-canary-app-6" },
|
alternativeBackends = { "my-dummy-canary-app-100" },
|
||||||
endpoints = { { address = "11.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
|
endpoints = { { address = "11.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
|
||||||
trafficShapingPolicy = {
|
trafficShapingPolicy = {
|
||||||
weight = 5,
|
weight = 5,
|
||||||
|
@ -112,7 +138,6 @@ describe("Balancer", function()
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_ngx({ var = { proxy_upstream_name = backend.name } })
|
mock_ngx({ var = { proxy_upstream_name = backend.name } })
|
||||||
reset_balancer()
|
|
||||||
|
|
||||||
balancer.sync_backend(backend)
|
balancer.sync_backend(backend)
|
||||||
balancer.sync_backend(canary_backend)
|
balancer.sync_backend(canary_backend)
|
||||||
|
@ -126,172 +151,223 @@ describe("Balancer", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("route_to_alternative_balancer()", function()
|
describe("route_to_alternative_balancer()", function()
|
||||||
local backend, _balancer
|
local backend, _primaryBalancer
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
backend = backends[1]
|
backend = backends[1]
|
||||||
_balancer = {
|
_primaryBalancer = {
|
||||||
alternative_backends = {
|
alternative_backends = {
|
||||||
backend.name,
|
backend.name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mock_ngx({ var = { request_uri = "/" } })
|
mock_ngx({ var = { request_uri = "/" } })
|
||||||
reset_balancer()
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("returns false when no trafficShapingPolicy is set", function()
|
-- Not affinitized request must follow traffic shaping policies.
|
||||||
balancer.sync_backend(backend)
|
describe("not affinitized", function()
|
||||||
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("returns false when no alternative backends is set", function()
|
before_each(function()
|
||||||
backend.trafficShapingPolicy.weight = 100
|
_primaryBalancer.is_affinitized = function (_)
|
||||||
balancer.sync_backend(backend)
|
return false
|
||||||
_balancer.alternative_backends = nil
|
end
|
||||||
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
|
end)
|
||||||
end)
|
|
||||||
|
|
||||||
it("returns false when alternative backends name does not match", function()
|
it("returns false when no trafficShapingPolicy is set", function()
|
||||||
backend.trafficShapingPolicy.weight = 100
|
balancer.sync_backend(backend)
|
||||||
balancer.sync_backend(backend)
|
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
_balancer.alternative_backends[1] = "nonExistingBackend"
|
end)
|
||||||
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
|
|
||||||
end)
|
|
||||||
|
|
||||||
context("canary by weight", function()
|
it("returns false when no alternative backends is set", function()
|
||||||
it("returns true when weight is 100", function()
|
|
||||||
backend.trafficShapingPolicy.weight = 100
|
backend.trafficShapingPolicy.weight = 100
|
||||||
balancer.sync_backend(backend)
|
balancer.sync_backend(backend)
|
||||||
assert.equal(true, balancer.route_to_alternative_balancer(_balancer))
|
_primaryBalancer.alternative_backends = nil
|
||||||
|
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("returns false when weight is 0", function()
|
it("returns false when alternative backends name does not match", function()
|
||||||
backend.trafficShapingPolicy.weight = 0
|
backend.trafficShapingPolicy.weight = 100
|
||||||
balancer.sync_backend(backend)
|
balancer.sync_backend(backend)
|
||||||
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
|
_primaryBalancer.alternative_backends[1] = "nonExistingBackend"
|
||||||
|
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("canary by weight", function()
|
||||||
|
it("returns true when weight is 100", function()
|
||||||
|
backend.trafficShapingPolicy.weight = 100
|
||||||
|
balancer.sync_backend(backend)
|
||||||
|
assert.equal(true, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns false when weight is 0", function()
|
||||||
|
backend.trafficShapingPolicy.weight = 0
|
||||||
|
balancer.sync_backend(backend)
|
||||||
|
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("canary by cookie", function()
|
||||||
|
it("returns correct result for given cookies", function()
|
||||||
|
local test_patterns = {
|
||||||
|
{
|
||||||
|
case_title = "cookie_value is 'always'",
|
||||||
|
request_cookie_name = "canaryCookie",
|
||||||
|
request_cookie_value = "always",
|
||||||
|
expected_result = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "cookie_value is 'never'",
|
||||||
|
request_cookie_name = "canaryCookie",
|
||||||
|
request_cookie_value = "never",
|
||||||
|
expected_result = false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "cookie_value is undefined",
|
||||||
|
request_cookie_name = "canaryCookie",
|
||||||
|
request_cookie_value = "foo",
|
||||||
|
expected_result = false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "cookie_name is undefined",
|
||||||
|
request_cookie_name = "foo",
|
||||||
|
request_cookie_value = "always",
|
||||||
|
expected_result = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test_pattern in pairs(test_patterns) do
|
||||||
|
mock_ngx({ var = {
|
||||||
|
["cookie_" .. test_pattern.request_cookie_name] = test_pattern.request_cookie_value,
|
||||||
|
request_uri = "/"
|
||||||
|
}})
|
||||||
|
backend.trafficShapingPolicy.cookie = "canaryCookie"
|
||||||
|
balancer.sync_backend(backend)
|
||||||
|
assert.message("\nTest data pattern: " .. test_pattern.case_title)
|
||||||
|
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
|
reset_ngx()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("canary by header", function()
|
||||||
|
it("returns correct result for given headers", function()
|
||||||
|
local test_patterns = {
|
||||||
|
-- with no header value setting
|
||||||
|
{
|
||||||
|
case_title = "no custom header value and header value is 'always'",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "",
|
||||||
|
request_header_name = "canaryHeader",
|
||||||
|
request_header_value = "always",
|
||||||
|
expected_result = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "no custom header value and header value is 'never'",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "",
|
||||||
|
request_header_name = "canaryHeader",
|
||||||
|
request_header_value = "never",
|
||||||
|
expected_result = false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "no custom header value and header value is undefined",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "",
|
||||||
|
request_header_name = "canaryHeader",
|
||||||
|
request_header_value = "foo",
|
||||||
|
expected_result = false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "no custom header value and header name is undefined",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "",
|
||||||
|
request_header_name = "foo",
|
||||||
|
request_header_value = "always",
|
||||||
|
expected_result = false,
|
||||||
|
},
|
||||||
|
-- with header value setting
|
||||||
|
{
|
||||||
|
case_title = "custom header value is set and header value is 'always'",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "foo",
|
||||||
|
request_header_name = "canaryHeader",
|
||||||
|
request_header_value = "always",
|
||||||
|
expected_result = false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "custom header value is set and header value match custom header value",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "foo",
|
||||||
|
request_header_name = "canaryHeader",
|
||||||
|
request_header_value = "foo",
|
||||||
|
expected_result = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
case_title = "custom header value is set and header name is undefined",
|
||||||
|
header_name = "canaryHeader",
|
||||||
|
header_value = "foo",
|
||||||
|
request_header_name = "bar",
|
||||||
|
request_header_value = "foo",
|
||||||
|
expected_result = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test_pattern in pairs(test_patterns) do
|
||||||
|
mock_ngx({ var = {
|
||||||
|
["http_" .. test_pattern.request_header_name] = test_pattern.request_header_value,
|
||||||
|
request_uri = "/"
|
||||||
|
}})
|
||||||
|
backend.trafficShapingPolicy.header = test_pattern.header_name
|
||||||
|
backend.trafficShapingPolicy.headerValue = test_pattern.header_value
|
||||||
|
balancer.sync_backend(backend)
|
||||||
|
assert.message("\nTest data pattern: " .. test_pattern.case_title)
|
||||||
|
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
|
reset_ngx()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
context("canary by cookie", function()
|
-- Affinitized request prefers backend it is affinitized to.
|
||||||
it("returns correct result for given cookies", function()
|
describe("affinitized", function()
|
||||||
local test_patterns = {
|
|
||||||
{
|
|
||||||
case_title = "cookie_value is 'always'",
|
|
||||||
request_cookie_name = "canaryCookie",
|
|
||||||
request_cookie_value = "always",
|
|
||||||
expected_result = true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "cookie_value is 'never'",
|
|
||||||
request_cookie_name = "canaryCookie",
|
|
||||||
request_cookie_value = "never",
|
|
||||||
expected_result = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "cookie_value is undefined",
|
|
||||||
request_cookie_name = "canaryCookie",
|
|
||||||
request_cookie_value = "foo",
|
|
||||||
expected_result = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "cookie_name is undefined",
|
|
||||||
request_cookie_name = "foo",
|
|
||||||
request_cookie_value = "always",
|
|
||||||
expected_result = false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test_pattern in pairs(test_patterns) do
|
|
||||||
mock_ngx({ var = {
|
|
||||||
["cookie_" .. test_pattern.request_cookie_name] = test_pattern.request_cookie_value,
|
|
||||||
request_uri = "/"
|
|
||||||
}})
|
|
||||||
reset_balancer()
|
|
||||||
backend.trafficShapingPolicy.cookie = "canaryCookie"
|
|
||||||
balancer.sync_backend(backend)
|
|
||||||
assert.message("\nTest data pattern: " .. test_pattern.case_title)
|
|
||||||
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_balancer))
|
|
||||||
reset_ngx()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
context("canary by header", function()
|
before_each(function()
|
||||||
it("returns correct result for given headers", function()
|
mock_ngx({ var = { request_uri = "/", proxy_upstream_name = backend.name } })
|
||||||
local test_patterns = {
|
balancer.sync_backend(backend)
|
||||||
-- with no header value setting
|
|
||||||
{
|
|
||||||
case_title = "no custom header value and header value is 'always'",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "",
|
|
||||||
request_header_name = "canaryHeader",
|
|
||||||
request_header_value = "always",
|
|
||||||
expected_result = true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "no custom header value and header value is 'never'",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "",
|
|
||||||
request_header_name = "canaryHeader",
|
|
||||||
request_header_value = "never",
|
|
||||||
expected_result = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "no custom header value and header value is undefined",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "",
|
|
||||||
request_header_name = "canaryHeader",
|
|
||||||
request_header_value = "foo",
|
|
||||||
expected_result = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "no custom header value and header name is undefined",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "",
|
|
||||||
request_header_name = "foo",
|
|
||||||
request_header_value = "always",
|
|
||||||
expected_result = false,
|
|
||||||
},
|
|
||||||
-- with header value setting
|
|
||||||
{
|
|
||||||
case_title = "custom header value is set and header value is 'always'",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "foo",
|
|
||||||
request_header_name = "canaryHeader",
|
|
||||||
request_header_value = "always",
|
|
||||||
expected_result = false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "custom header value is set and header value match custom header value",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "foo",
|
|
||||||
request_header_name = "canaryHeader",
|
|
||||||
request_header_value = "foo",
|
|
||||||
expected_result = true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
case_title = "custom header value is set and header name is undefined",
|
|
||||||
header_name = "canaryHeader",
|
|
||||||
header_value = "foo",
|
|
||||||
request_header_name = "bar",
|
|
||||||
request_header_value = "foo",
|
|
||||||
expected_result = false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test_pattern in pairs(test_patterns) do
|
|
||||||
mock_ngx({ var = {
|
|
||||||
["http_" .. test_pattern.request_header_name] = test_pattern.request_header_value,
|
|
||||||
request_uri = "/"
|
|
||||||
}})
|
|
||||||
reset_balancer()
|
|
||||||
backend.trafficShapingPolicy.header = test_pattern.header_name
|
|
||||||
backend.trafficShapingPolicy.headerValue = test_pattern.header_value
|
|
||||||
balancer.sync_backend(backend)
|
|
||||||
assert.message("\nTest data pattern: " .. test_pattern.case_title)
|
|
||||||
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_balancer))
|
|
||||||
reset_ngx()
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("returns false if request is affinitized to primary backend", function()
|
||||||
|
_primaryBalancer.is_affinitized = function (_)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local alternativeBalancer = balancer.get_balancer_by_upstream_name(backend.name)
|
||||||
|
|
||||||
|
local primarySpy = spy.on(_primaryBalancer, "is_affinitized")
|
||||||
|
local alternativeSpy = spy.on(alternativeBalancer, "is_affinitized")
|
||||||
|
|
||||||
|
assert.is_false(balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
|
assert.spy(_primaryBalancer.is_affinitized).was_called()
|
||||||
|
assert.spy(alternativeBalancer.is_affinitized).was_not_called()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns true if request is affinitized to alternative backend", function()
|
||||||
|
_primaryBalancer.is_affinitized = function (_)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local alternativeBalancer = balancer.get_balancer_by_upstream_name(backend.name)
|
||||||
|
alternativeBalancer.is_affinitized = function (_)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local primarySpy = spy.on(_primaryBalancer, "is_affinitized")
|
||||||
|
local alternativeSpy = spy.on(alternativeBalancer, "is_affinitized")
|
||||||
|
|
||||||
|
assert.is_true(balancer.route_to_alternative_balancer(_primaryBalancer))
|
||||||
|
assert.spy(_primaryBalancer.is_affinitized).was_called()
|
||||||
|
assert.spy(alternativeBalancer.is_affinitized).was_called()
|
||||||
|
end)
|
||||||
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -432,10 +508,13 @@ describe("Balancer", function()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mock_ngx({ var = { proxy_upstream_name = "access-router-production-web-80" }, ctx = { } })
|
|
||||||
ngx.shared.configuration_data:set("backends", cjson.encode(backends))
|
mock_ngx({ var = { proxy_upstream_name = "access-router-production-web-80" }, ctx = { } }, function()
|
||||||
reset_balancer()
|
ngx.shared.configuration_data:set("backends", cjson.encode(backends))
|
||||||
|
end)
|
||||||
|
|
||||||
balancer.init_worker()
|
balancer.init_worker()
|
||||||
|
|
||||||
assert.not_equal(balancer.get_balancer(), nil)
|
assert.not_equal(balancer.get_balancer(), nil)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
local split = require("util.split")
|
|
||||||
|
|
||||||
|
|
||||||
describe("split", function()
|
|
||||||
it("get_last_value", function()
|
|
||||||
for _, case in ipairs({
|
|
||||||
{"127.0.0.1:26157 : 127.0.0.1:26158", "127.0.0.1:26158"},
|
|
||||||
{"127.0.0.1:26157, 127.0.0.1:26158", "127.0.0.1:26158"},
|
|
||||||
{"127.0.0.1:26158", "127.0.0.1:26158"},
|
|
||||||
}) do
|
|
||||||
local last = split.get_last_value(case[1])
|
|
||||||
assert.equal(case[2], last)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
57
rootfs/etc/nginx/lua/test/util/split_test.lua
Normal file
57
rootfs/etc/nginx/lua/test/util/split_test.lua
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
local split = require("util.split")
|
||||||
|
|
||||||
|
describe("split", function()
|
||||||
|
|
||||||
|
describe("get_last_value", function()
|
||||||
|
it("splits value of an upstream variable and returns last value", function()
|
||||||
|
for _, case in ipairs({{"127.0.0.1:26157 : 127.0.0.1:26158", "127.0.0.1:26158"},
|
||||||
|
{"127.0.0.1:26157, 127.0.0.1:26158", "127.0.0.1:26158"},
|
||||||
|
{"127.0.0.1:26158", "127.0.0.1:26158"}}) do
|
||||||
|
local last = split.get_last_value(case[1])
|
||||||
|
assert.equal(case[2], last)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("split_string", function()
|
||||||
|
|
||||||
|
it("returns empty array if input string is empty", function()
|
||||||
|
local splits, len = split.split_string("", ",")
|
||||||
|
assert.equal(0, len)
|
||||||
|
assert.is.truthy(splits)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns empty array if input string is nil", function()
|
||||||
|
local splits, len = split.split_string(nil, ",")
|
||||||
|
assert.equal(0, len)
|
||||||
|
assert.is.truthy(splits)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns empty array if delimiter is empty", function()
|
||||||
|
local splits, len = split.split_string("1,2", "")
|
||||||
|
assert.equal(0, len)
|
||||||
|
assert.is.truthy(splits)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns empty array delimiter is nil", function()
|
||||||
|
local splits, len = split.split_string("1,2", nil)
|
||||||
|
assert.equal(0, len)
|
||||||
|
assert.is.truthy(splits)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns array of 1 value if input string is not a list", function()
|
||||||
|
local splits, len = split.split_string("123", ",")
|
||||||
|
assert.equal(1, len)
|
||||||
|
assert.equal("123", splits[1])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns array of values extracted from the input string", function()
|
||||||
|
local splits, len = split.split_string("1,2,3", ",")
|
||||||
|
assert.equal(3, len)
|
||||||
|
assert.equal("1", splits[1])
|
||||||
|
assert.equal("2", splits[2])
|
||||||
|
assert.equal("3", splits[3])
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
end)
|
|
@ -65,4 +65,19 @@ function _M.split_upstream_addr(addrs_str)
|
||||||
return host_and_ports
|
return host_and_ports
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Splits string by delimiter. Returns array of parsed values and the length of the array.
|
||||||
|
function _M.split_string(what, delim)
|
||||||
|
local result = {}
|
||||||
|
local idx = 0
|
||||||
|
|
||||||
|
if what and delim and delim ~= "" then
|
||||||
|
for chunk in what:gmatch("([^" .. delim .. "]+)") do
|
||||||
|
idx = idx + 1
|
||||||
|
result[idx] = chunk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result, idx
|
||||||
|
end
|
||||||
|
|
||||||
return _M
|
return _M
|
||||||
|
|
Loading…
Reference in a new issue