Skip to content

Commit fbb96f4

Browse files
authored
Merge pull request #981 from chrismoos/service_upstream
Add annotation to allow use of service ClusterIP for NGINX upstream.
2 parents e59ac13 + 666bcca commit fbb96f4

File tree

5 files changed

+227
-10
lines changed

5 files changed

+227
-10
lines changed

controllers/nginx/configuration.md

+12
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ The following annotations are supported:
5757
|[ingress.kubernetes.io/proxy-body-size](#custom-max-body-size)|string|
5858
|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI|
5959
|[ingress.kubernetes.io/secure-backends](#secure-backends)|true or false|
60+
|[ingress.kubernetes.io/service-upstream](#service-upstream)|true or false|
6061
|[ingress.kubernetes.io/session-cookie-name](#cookie-affinity)|string|
6162
|[ingress.kubernetes.io/session-cookie-hash](#cookie-affinity)|string|
6263
|[ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|true or false|
@@ -213,6 +214,17 @@ This is possible thanks to the [ngx_stream_ssl_preread_module](https://nginx.org
213214

214215
By default NGINX uses `http` to reach the services. Adding the annotation `ingress.kubernetes.io/secure-backends: "true"` in the Ingress rule changes the protocol to `https`.
215216

217+
### Service Upstream
218+
219+
By default the NGINX ingress controller uses a list of all endpoints (Pod IP/port) in the NGINX upstream configuration. This annotation disables that behavior and instead uses a single upstream in NGINX, the service's Cluster IP and port. This can be desirable for things like zero-downtime deployments as it reduces the need to reload NGINX configuration when Pods come up and down. See issue [#257](https://github.com/kubernetes/ingress/issues/257).
220+
221+
222+
#### Known Issues
223+
224+
If the `service-upstream` annotation is specified the following things should be taken into consideration:
225+
226+
* Sticky Sessions will not work as only round-robin load balancing is supported.
227+
* The `proxy_next_upstream` directive will not have any effect meaning on error the request will not be dispatched to another upstream.
216228

217229
### Server-side HTTPS enforcement through redirect
218230

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package serviceupstream
18+
19+
import (
20+
extensions "k8s.io/client-go/pkg/apis/extensions/v1beta1"
21+
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
22+
)
23+
24+
const (
25+
annotationServiceUpstream = "ingress.kubernetes.io/service-upstream"
26+
)
27+
28+
type serviceUpstream struct {
29+
}
30+
31+
// NewParser creates a new serviceUpstream annotation parser
32+
func NewParser() parser.IngressAnnotation {
33+
return serviceUpstream{}
34+
}
35+
36+
func (s serviceUpstream) Parse(ing *extensions.Ingress) (interface{}, error) {
37+
return parser.GetBoolAnnotation(annotationServiceUpstream, ing)
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package serviceupstream
18+
19+
import (
20+
"testing"
21+
22+
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/util/intstr"
24+
api "k8s.io/client-go/pkg/api/v1"
25+
extensions "k8s.io/client-go/pkg/apis/extensions/v1beta1"
26+
)
27+
28+
func buildIngress() *extensions.Ingress {
29+
defaultBackend := extensions.IngressBackend{
30+
ServiceName: "default-backend",
31+
ServicePort: intstr.FromInt(80),
32+
}
33+
34+
return &extensions.Ingress{
35+
ObjectMeta: meta_v1.ObjectMeta{
36+
Name: "foo",
37+
Namespace: api.NamespaceDefault,
38+
},
39+
Spec: extensions.IngressSpec{
40+
Backend: &extensions.IngressBackend{
41+
ServiceName: "default-backend",
42+
ServicePort: intstr.FromInt(80),
43+
},
44+
Rules: []extensions.IngressRule{
45+
{
46+
Host: "foo.bar.com",
47+
IngressRuleValue: extensions.IngressRuleValue{
48+
HTTP: &extensions.HTTPIngressRuleValue{
49+
Paths: []extensions.HTTPIngressPath{
50+
{
51+
Path: "/foo",
52+
Backend: defaultBackend,
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
}
61+
}
62+
63+
func TestIngressAnnotationServiceUpstreamEnabled(t *testing.T) {
64+
ing := buildIngress()
65+
66+
data := map[string]string{}
67+
data[annotationServiceUpstream] = "true"
68+
ing.SetAnnotations(data)
69+
70+
val, _ := NewParser().Parse(ing)
71+
enabled, ok := val.(bool)
72+
if !ok {
73+
t.Errorf("expected a bool type")
74+
}
75+
76+
if !enabled {
77+
t.Errorf("expected annotation value to be true, got false")
78+
}
79+
}
80+
81+
func TestIngressAnnotationServiceUpstreamSetFalse(t *testing.T) {
82+
ing := buildIngress()
83+
84+
// Test with explicitly set to false
85+
data := map[string]string{}
86+
data[annotationServiceUpstream] = "false"
87+
ing.SetAnnotations(data)
88+
89+
val, _ := NewParser().Parse(ing)
90+
enabled, ok := val.(bool)
91+
if !ok {
92+
t.Errorf("expected a bool type")
93+
}
94+
95+
if enabled {
96+
t.Errorf("expected annotation value to be false, got true")
97+
}
98+
99+
// Test with no annotation specified, should default to false
100+
data = map[string]string{}
101+
ing.SetAnnotations(data)
102+
103+
val, _ = NewParser().Parse(ing)
104+
enabled, ok = val.(bool)
105+
if !ok {
106+
t.Errorf("expected a bool type")
107+
}
108+
109+
if enabled {
110+
t.Errorf("expected annotation value to be false, got true")
111+
}
112+
}

core/pkg/ingress/controller/annotations.go

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
3232
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
3333
"k8s.io/ingress/core/pkg/ingress/annotations/secureupstream"
34+
"k8s.io/ingress/core/pkg/ingress/annotations/serviceupstream"
3435
"k8s.io/ingress/core/pkg/ingress/annotations/sessionaffinity"
3536
"k8s.io/ingress/core/pkg/ingress/annotations/snippet"
3637
"k8s.io/ingress/core/pkg/ingress/annotations/sslpassthrough"
@@ -64,6 +65,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
6465
"RateLimit": ratelimit.NewParser(),
6566
"Redirect": rewrite.NewParser(cfg),
6667
"SecureUpstream": secureupstream.NewParser(cfg),
68+
"ServiceUpstream": serviceupstream.NewParser(),
6769
"SessionAffinity": sessionaffinity.NewParser(),
6870
"SSLPassthrough": sslpassthrough.NewParser(),
6971
"ConfigurationSnippet": snippet.NewParser(),
@@ -104,8 +106,14 @@ const (
104106
healthCheck = "HealthCheck"
105107
sslPassthrough = "SSLPassthrough"
106108
sessionAffinity = "SessionAffinity"
109+
serviceUpstream = "ServiceUpstream"
107110
)
108111

112+
func (e *annotationExtractor) ServiceUpstream(ing *extensions.Ingress) bool {
113+
val, _ := e.annotations[serviceUpstream].Parse(ing)
114+
return val.(bool)
115+
}
116+
109117
func (e *annotationExtractor) SecureUpstream(ing *extensions.Ingress) *secureupstream.Secure {
110118
val, err := e.annotations[secureUpstream].Parse(ing)
111119
if err != nil {

core/pkg/ingress/controller/controller.go

+57-10
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing
782782

783783
secUpstream := ic.annotations.SecureUpstream(ing)
784784
hz := ic.annotations.HealthCheck(ing)
785+
serviceUpstream := ic.annotations.ServiceUpstream(ing)
785786

786787
var defBackend string
787788
if ing.Spec.Backend != nil {
@@ -792,13 +793,27 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing
792793

793794
glog.V(3).Infof("creating upstream %v", defBackend)
794795
upstreams[defBackend] = newUpstream(defBackend)
795-
796796
svcKey := fmt.Sprintf("%v/%v", ing.GetNamespace(), ing.Spec.Backend.ServiceName)
797-
endps, err := ic.serviceEndpoints(svcKey, ing.Spec.Backend.ServicePort.String(), hz)
798-
upstreams[defBackend].Endpoints = append(upstreams[defBackend].Endpoints, endps...)
799-
if err != nil {
800-
glog.Warningf("error creating upstream %v: %v", defBackend, err)
797+
798+
// Add the service cluster endpoint as the upstream instead of individual endpoints
799+
// if the serviceUpstream annotation is enabled
800+
if serviceUpstream {
801+
endpoint, err := ic.getServiceClusterEndpoint(svcKey, ing.Spec.Backend)
802+
if err != nil {
803+
glog.Errorf("Failed to get service cluster endpoint for service %s: %v", svcKey, err)
804+
} else {
805+
upstreams[defBackend].Endpoints = []ingress.Endpoint{endpoint}
806+
}
801807
}
808+
809+
if len(upstreams[defBackend].Endpoints) == 0 {
810+
endps, err := ic.serviceEndpoints(svcKey, ing.Spec.Backend.ServicePort.String(), hz)
811+
upstreams[defBackend].Endpoints = append(upstreams[defBackend].Endpoints, endps...)
812+
if err != nil {
813+
glog.Warningf("error creating upstream %v: %v", defBackend, err)
814+
}
815+
}
816+
802817
}
803818

804819
for _, rule := range ing.Spec.Rules {
@@ -827,12 +842,26 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing
827842
}
828843

829844
svcKey := fmt.Sprintf("%v/%v", ing.GetNamespace(), path.Backend.ServiceName)
830-
endp, err := ic.serviceEndpoints(svcKey, path.Backend.ServicePort.String(), hz)
831-
if err != nil {
832-
glog.Warningf("error obtaining service endpoints: %v", err)
833-
continue
845+
846+
// Add the service cluster endpoint as the upstream instead of individual endpoints
847+
// if the serviceUpstream annotation is enabled
848+
if serviceUpstream {
849+
endpoint, err := ic.getServiceClusterEndpoint(svcKey, &path.Backend)
850+
if err != nil {
851+
glog.Errorf("Failed to get service cluster endpoint for service %s: %v", svcKey, err)
852+
} else {
853+
upstreams[name].Endpoints = []ingress.Endpoint{endpoint}
854+
}
855+
}
856+
857+
if len(upstreams[name].Endpoints) == 0 {
858+
endp, err := ic.serviceEndpoints(svcKey, path.Backend.ServicePort.String(), hz)
859+
if err != nil {
860+
glog.Warningf("error obtaining service endpoints: %v", err)
861+
continue
862+
}
863+
upstreams[name].Endpoints = endp
834864
}
835-
upstreams[name].Endpoints = endp
836865

837866
s, exists, err := ic.svcLister.Store.GetByKey(svcKey)
838867
if err != nil {
@@ -853,6 +882,24 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing
853882
return upstreams
854883
}
855884

885+
func (ic *GenericController) getServiceClusterEndpoint(svcKey string, backend *extensions.IngressBackend) (endpoint ingress.Endpoint, err error) {
886+
svcObj, svcExists, err := ic.svcLister.Store.GetByKey(svcKey)
887+
888+
if !svcExists {
889+
return endpoint, fmt.Errorf("service %v does not exist", svcKey)
890+
}
891+
892+
svc := svcObj.(*api.Service)
893+
if svc.Spec.ClusterIP == "" {
894+
return endpoint, fmt.Errorf("No ClusterIP found for service %s", svcKey)
895+
}
896+
897+
endpoint.Address = svc.Spec.ClusterIP
898+
endpoint.Port = backend.ServicePort.String()
899+
900+
return endpoint, err
901+
}
902+
856903
// serviceEndpoints returns the upstream servers (endpoints) associated
857904
// to a service.
858905
func (ic *GenericController) serviceEndpoints(svcKey, backendPort string,

0 commit comments

Comments
 (0)