diff --git a/core/pkg/ingress/annotations/stickysession/main.go b/core/pkg/ingress/annotations/stickysession/main.go new file mode 100644 index 000000000..be4dd2f0f --- /dev/null +++ b/core/pkg/ingress/annotations/stickysession/main.go @@ -0,0 +1,96 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stickysession + +import ( + "regexp" + + "k8s.io/kubernetes/pkg/apis/extensions" + + "k8s.io/ingress/core/pkg/ingress/annotations/parser" + ing_errors "k8s.io/ingress/core/pkg/ingress/errors" +) + +const ( + stickyEnabled = "ingress.kubernetes.io/sticky-enabled" + stickyName = "ingress.kubernetes.io/sticky-name" + stickyHash = "ingress.kubernetes.io/sticky-hash" + defaultStickyHash = "md5" + defaultStickyName = "route" +) + +var ( + stickyHashRegex = regexp.MustCompile(`index|md5|sha1`) +) + +// StickyConfig describes the per ingress sticky session config +type StickyConfig struct { + // The name of the cookie that will be used as stickness router. + Name string `json:"name"` + // If sticky must or must not be enabled + Enabled bool `json:"enabled"` + // The hash that will be used to encode the cookie + Hash string `json:"hash"` +} + +type sticky struct { +} + +// NewParser creates a new Sticky annotation parser +func NewParser() parser.IngressAnnotation { + return sticky{} +} + +// ParseAnnotations parses the annotations contained in the ingress +// rule used to configure the sticky directives +func (a sticky) Parse(ing *extensions.Ingress) (interface{}, error) { + // Check if the sticky is enabled + se, err := parser.GetBoolAnnotation(stickyEnabled, ing) + if err != nil { + return nil, err + } + + // Get the Sticky Cookie Name + sn, err := parser.GetStringAnnotation(stickyName, ing) + if err != nil { + return nil, err + } + + if sn == "" { + sn = defaultStickyName + } + + sh, err := parser.GetStringAnnotation(stickyHash, ing) + + if err != nil { + return nil, err + } + + if sh == "" { + sh = defaultStickyHash + } + + if !stickyHashRegex.MatchString(sh) { + return nil, ing_errors.NewInvalidAnnotationContent(stickyHash, sh) + } + + return &StickyConfig{ + Name: sn, + Enabled: se, + Hash: sh, + }, nil +} diff --git a/core/pkg/ingress/annotations/stickysession/main_test.go b/core/pkg/ingress/annotations/stickysession/main_test.go new file mode 100644 index 000000000..a8a98f3c4 --- /dev/null +++ b/core/pkg/ingress/annotations/stickysession/main_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stickysession + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/intstr" +) + +func buildIngress() *extensions.Ingress { + defaultBackend := extensions.IngressBackend{ + ServiceName: "default-backend", + ServicePort: intstr.FromInt(80), + } + + return &extensions.Ingress{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: extensions.IngressSpec{ + Backend: &extensions.IngressBackend{ + ServiceName: "default-backend", + ServicePort: intstr.FromInt(80), + }, + Rules: []extensions.IngressRule{ + { + Host: "foo.bar.com", + IngressRuleValue: extensions.IngressRuleValue{ + HTTP: &extensions.HTTPIngressRuleValue{ + Paths: []extensions.HTTPIngressPath{ + { + Path: "/foo", + Backend: defaultBackend, + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestIngressHealthCheck(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[stickyEnabled] = "true" + data[stickyHash] = "md5" + data[stickyName] = "route1" + ing.SetAnnotations(data) + + sti, _ := NewParser().Parse(ing) + nginxSti, ok := sti.(*StickyConfig) + if !ok { + t.Errorf("expected a StickyConfig type") + } + + if nginxSti.Hash != "md5" { + t.Errorf("expected md5 as sticky-hash but returned %v", nginxSti.Hash) + } + + if nginxSti.Hash != "md5" { + t.Errorf("expected md5 as sticky-hash but returned %v", nginxSti.Hash) + } + + if !nginxSti.Enabled { + t.Errorf("expected sticky-enabled but returned %v", nginxSti.Enabled) + } +} diff --git a/core/pkg/ingress/controller/annotations.go b/core/pkg/ingress/controller/annotations.go index c1eb09fbd..400776866 100644 --- a/core/pkg/ingress/controller/annotations.go +++ b/core/pkg/ingress/controller/annotations.go @@ -34,6 +34,7 @@ import ( "k8s.io/ingress/core/pkg/ingress/annotations/rewrite" "k8s.io/ingress/core/pkg/ingress/annotations/secureupstream" "k8s.io/ingress/core/pkg/ingress/annotations/sslpassthrough" + "k8s.io/ingress/core/pkg/ingress/annotations/stickysession" "k8s.io/ingress/core/pkg/ingress/errors" "k8s.io/ingress/core/pkg/ingress/resolver" ) @@ -63,6 +64,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor { "Redirect": rewrite.NewParser(cfg), "SecureUpstream": secureupstream.NewParser(), "SSLPassthrough": sslpassthrough.NewParser(), + "StickySession": stickysession.NewParser(), }, } } @@ -99,6 +101,7 @@ const ( secureUpstream = "SecureUpstream" healthCheck = "HealthCheck" sslPassthrough = "SSLPassthrough" + stickySession = "StickySession" ) func (e *annotationExtractor) SecureUpstream(ing *extensions.Ingress) bool { @@ -115,3 +118,8 @@ func (e *annotationExtractor) SSLPassthrough(ing *extensions.Ingress) bool { val, _ := e.annotations[sslPassthrough].Parse(ing) return val.(bool) } + +func (e *annotationExtractor) StickySession(ing *extensions.Ingress) *stickysession.StickyConfig { + val, _ := e.annotations[stickySession].Parse(ing) + return val.(*stickysession.StickyConfig) +} diff --git a/core/pkg/ingress/controller/annotations_test.go b/core/pkg/ingress/controller/annotations_test.go index 58b0d8093..942508bab 100644 --- a/core/pkg/ingress/controller/annotations_test.go +++ b/core/pkg/ingress/controller/annotations_test.go @@ -32,6 +32,9 @@ const ( annotationUpsMaxFails = "ingress.kubernetes.io/upstream-max-fails" annotationUpsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout" annotationPassthrough = "ingress.kubernetes.io/ssl-passthrough" + annotationStickyEnabled = "ingress.kubernetes.io/sticky-enabled" + annotationStickyName = "ingress.kubernetes.io/sticky-name" + annotationStickyHash = "ingress.kubernetes.io/sticky-hash" ) type mockCfg struct { @@ -179,3 +182,37 @@ func TestSSLPassthrough(t *testing.T) { } } } + +func TestStickySession(t *testing.T) { + ec := newAnnotationExtractor(mockCfg{}) + ing := buildIngress() + + fooAnns := []struct { + annotations map[string]string + enabled bool + hash string + name string + }{ + {map[string]string{annotationStickyEnabled: "true", annotationStickyHash: "md5", annotationStickyName: "route"}, true, "md5", "route"}, + {map[string]string{annotationStickyEnabled: "true", annotationStickyHash: "", annotationStickyName: "xpto"}, true, "md5", "xpto"}, + {map[string]string{annotationStickyEnabled: "true", annotationStickyHash: "", annotationStickyName: ""}, true, "md5", "route"}, + } + + for _, foo := range fooAnns { + ing.SetAnnotations(foo.annotations) + r := ec.StickySession(ing) + + if r == nil { + t.Errorf("Returned nil but expected a StickySesion.StickyConfig") + continue + } + + if r.Hash != foo.hash { + t.Errorf("Returned %v but expected %v for Hash", r.Hash, foo.hash) + } + + if r.Name != foo.name { + t.Errorf("Returned %v but expected %v for Name", r.Name, foo.name) + } + } +} diff --git a/core/pkg/ingress/controller/controller.go b/core/pkg/ingress/controller/controller.go index 5c979dcce..ace3049b1 100644 --- a/core/pkg/ingress/controller/controller.go +++ b/core/pkg/ingress/controller/controller.go @@ -700,6 +700,7 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing secUpstream := ic.annotations.SecureUpstream(ing) hz := ic.annotations.HealthCheck(ing) + sticky := ic.annotations.StickySession(ing) var defBackend string if ing.Spec.Backend != nil { @@ -739,6 +740,12 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing if !upstreams[name].Secure { upstreams[name].Secure = secUpstream } + if !upstreams[name].StickySession.Enabled || upstreams[name].StickySession.Name == "" || upstreams[name].StickySession.Hash == "" { + upstreams[name].StickySession.Enabled = sticky.Enabled + upstreams[name].StickySession.Name = sticky.Name + upstreams[name].StickySession.Hash = sticky.Hash + } + svcKey := fmt.Sprintf("%v/%v", ing.GetNamespace(), path.Backend.ServiceName) endp, err := ic.serviceEndpoints(svcKey, path.Backend.ServicePort.String(), hz) if err != nil { diff --git a/core/pkg/ingress/types.go b/core/pkg/ingress/types.go index 49f849a57..89f426a89 100644 --- a/core/pkg/ingress/types.go +++ b/core/pkg/ingress/types.go @@ -26,6 +26,7 @@ import ( "k8s.io/ingress/core/pkg/ingress/annotations/proxy" "k8s.io/ingress/core/pkg/ingress/annotations/ratelimit" "k8s.io/ingress/core/pkg/ingress/annotations/rewrite" + "k8s.io/ingress/core/pkg/ingress/annotations/stickysession" "k8s.io/ingress/core/pkg/ingress/defaults" "k8s.io/ingress/core/pkg/ingress/resolver" ) @@ -134,6 +135,8 @@ type Backend struct { Secure bool `json:"secure"` // Endpoints contains the list of endpoints currently running Endpoints []Endpoint `json:"endpoints"` + // StickySession contains the StickyConfig object with stickness configuration + StickySession *stickysession.StickyConfig `json:"stickysession"` } // Endpoint describes a kubernetes endpoint in an backend