Skip to content

Commit 1bccb05

Browse files
author
Fernando Diaz
committed
Handle annotations and conflicting paths for MergeableTypes
Adds new rules for the MergeableTypes. Minions will not be able to have conflicting locations, and can only have service level annotations. Masters will only be able to have host level annotations. Fixes #264
1 parent 1e9cedb commit 1bccb05

File tree

6 files changed

+280
-10
lines changed

6 files changed

+280
-10
lines changed

examples/mergable-ingress-types/README.md

+25-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,32 @@ host level, which includes the TLS configuration, and any annotations which will
1010
can only be one ingress resource on a unique host that contains the master value. Paths cannot be part of the
1111
ingress resource.
1212

13+
Masters cannot contain the following annotations:
14+
* nginx.org/rewrites
15+
* nginx.org/ssl-services
16+
* nginx.org/websocket-services
17+
* nginx.com/sticky-cookie-services
18+
1319
A Minion is declared using `nginx.org/mergible-ingress-type: minion`. A Minion will be used to append different
14-
locations to an ingress resource with the Master value. The annotations of minions are replaced with the annotations of
15-
their master. TLS configurations are not allowed. There can be multiple minions which must have the same host as the master.
20+
locations to an ingress resource with the Master value. TLS configurations are not allowed. Multiple minions can be
21+
applied per master as long as they do not have conflicting paths. If a conflicting path is present then the path defined
22+
on the oldest minion will be used.
23+
24+
Minions cannot contain the following annotations:
25+
* nginx.org/proxy-hide-headers
26+
* nginx.org/proxy-pass-headers
27+
* nginx.org/redirect-to-https
28+
* ingress.kubernetes.io/ssl-redirect
29+
* nginx.org/hsts
30+
* nginx.org/hsts-max-age
31+
* nginx.org/hsts-include-subdomains
32+
* nginx.org/server-tokens
33+
* nginx.org/listen-ports
34+
* nginx.org/listen-ports-ssl
35+
* nginx.com/jwt-key
36+
* nginx.com/jwt-realm
37+
* nginx.com/jwt-token
38+
* nginx.com/jwt-login-url
1639

1740
Note: Ingress Resources with more than one host cannot be used.
1841

nginx-controller/controller/controller.go

+23
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
api_v1 "k8s.io/api/core/v1"
3838
extensions "k8s.io/api/extensions/v1beta1"
3939
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
40+
"sort"
4041
)
4142

4243
const (
@@ -1203,7 +1204,14 @@ func (lbc *LoadBalancerController) getMinionsForMaster(master *nginx.IngressEx)
12031204
return []*nginx.IngressEx{}, err
12041205
}
12051206

1207+
// ingresses are sorted by creation time
1208+
sort.Slice(ings.Items[:], func(i, j int) bool {
1209+
return ings.Items[i].CreationTimestamp.Time.UnixNano() < ings.Items[j].CreationTimestamp.Time.UnixNano()
1210+
})
1211+
12061212
var minions []*nginx.IngressEx
1213+
var minionPaths = make(map[string]*extensions.Ingress)
1214+
12071215
for i, _ := range ings.Items {
12081216
if !lbc.isNginxIngress(&ings.Items[i]) {
12091217
continue
@@ -1222,6 +1230,20 @@ func (lbc *LoadBalancerController) getMinionsForMaster(master *nginx.IngressEx)
12221230
glog.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergible-ingress-type' annotation set to 'minion' must contain a Path", ings.Items[i].Namespace, ings.Items[i].Name)
12231231
continue
12241232
}
1233+
1234+
uniquePaths := []extensions.HTTPIngressPath{}
1235+
for _, path := range ings.Items[i].Spec.Rules[0].HTTP.Paths {
1236+
if val, ok := minionPaths[path.Path]; ok {
1237+
glog.Errorf("Ingress Resource %v/%v with the 'nginx.org/mergible-ingress-type' annotation set to 'minion' cannot contain the same path as another ingress resource, %v/%v.",
1238+
ings.Items[i].Namespace, ings.Items[i].Name, val.Namespace, val.Name)
1239+
glog.Errorf("Path %s for Ingress Resource %v/%v will be ignored", path.Path, val.Namespace, val.Name)
1240+
} else {
1241+
minionPaths[path.Path] = &ings.Items[i]
1242+
uniquePaths = append(uniquePaths, path)
1243+
}
1244+
}
1245+
ings.Items[i].Spec.Rules[0].HTTP.Paths = uniquePaths
1246+
12251247
ingEx, err := lbc.createIngress(&ings.Items[i])
12261248
if err != nil {
12271249
glog.Errorf("Error creating ingress resource %v/%v: %v", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err)
@@ -1233,6 +1255,7 @@ func (lbc *LoadBalancerController) getMinionsForMaster(master *nginx.IngressEx)
12331255
}
12341256
minions = append(minions, ingEx)
12351257
}
1258+
12361259
return minions, nil
12371260
}
12381261

nginx-controller/controller/controller_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,65 @@ func TestGetMinionsForMasterInvalidMinion(t *testing.T) {
401401
}
402402
}
403403

404+
func TestGetMinionsForMasterConflictingPaths(t *testing.T) {
405+
cafeMaster, coffeeMinion, teaMinion, lbc := getMergableDefaults()
406+
407+
// Makes sure there is an empty path assigned to a master, to allow for lbc.createIngress() to pass
408+
cafeMaster.Spec.Rules[0].HTTP = &extensions.HTTPIngressRuleValue{
409+
Paths: []extensions.HTTPIngressPath{},
410+
}
411+
412+
coffeeMinion.Spec.Rules[0].HTTP.Paths = append(coffeeMinion.Spec.Rules[0].HTTP.Paths, extensions.HTTPIngressPath{
413+
Path: "/tea",
414+
Backend: extensions.IngressBackend{
415+
ServiceName: "tea-svc",
416+
ServicePort: intstr.IntOrString{
417+
StrVal: "80",
418+
},
419+
},
420+
})
421+
422+
lbc.ingLister.Add(&cafeMaster)
423+
lbc.ingLister.Add(&coffeeMinion)
424+
lbc.ingLister.Add(&teaMinion)
425+
426+
cafeMasterIngEx, err := lbc.createIngress(&cafeMaster)
427+
if err != nil {
428+
t.Errorf("Error creating %s(Master): %v", cafeMaster.Name, err)
429+
}
430+
431+
minions, err := lbc.getMinionsForMaster(cafeMasterIngEx)
432+
if err != nil {
433+
t.Errorf("Error getting Minions for %s(Master): %v", cafeMaster.Name, err)
434+
}
435+
436+
if len(minions) != 2 {
437+
t.Errorf("Invalid amount of minions: %+v", minions)
438+
}
439+
440+
coffeePathCount := 0
441+
teaPathCount := 0
442+
for _, minion := range minions {
443+
for _, path := range minion.Ingress.Spec.Rules[0].HTTP.Paths {
444+
if path.Path == "/coffee" {
445+
coffeePathCount++
446+
} else if path.Path == "/tea" {
447+
teaPathCount++
448+
} else {
449+
t.Errorf("Invalid Path %s exists", path.Path)
450+
}
451+
}
452+
}
453+
454+
if coffeePathCount != 1 {
455+
t.Errorf("Invalid amount of coffee paths, amount %d", coffeePathCount)
456+
}
457+
458+
if teaPathCount != 1 {
459+
t.Errorf("Invalid amount of tea paths, amount %d", teaPathCount)
460+
}
461+
}
462+
404463
func getMergableDefaults() (cafeMaster, coffeeMinion, teaMinion extensions.Ingress, lbc LoadBalancerController) {
405464
cafeMaster = extensions.Ingress{
406465
TypeMeta: meta_v1.TypeMeta{},

nginx-controller/nginx/configurator.go

+64-7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ func (cnf *Configurator) addOrUpdateMergableIngress(mergeableIngs *MergeableIngr
8383
var locations []Location
8484
var upstreams []Upstream
8585
var keepalive string
86+
var removedAnnotations []string
87+
88+
removedAnnotations = filterMasterAnnotations(mergeableIngs.Master.Ingress.Annotations)
89+
if len(removedAnnotations) != 0 {
90+
glog.Errorf("Ingress Resource %v/%v with the annotation 'nginx.com/mergeable-ingress-type' set to 'master' cannot contain the '%v' annotation(s). They will be ignored",
91+
mergeableIngs.Master.Ingress.Namespace, mergeableIngs.Master.Ingress.Name, strings.Join(removedAnnotations, ","))
92+
}
8693

8794
pems, jwtKeyFileName := cnf.updateSecrets(mergeableIngs.Master)
8895
masterNginxCfg := cnf.generateNginxCfg(mergeableIngs.Master, pems, jwtKeyFileName)
@@ -101,20 +108,28 @@ func (cnf *Configurator) addOrUpdateMergableIngress(mergeableIngs *MergeableIngr
101108

102109
minions := mergeableIngs.Minions
103110
for _, minion := range minions {
111+
// Remove the default backend so that "/" will not be generated
112+
minion.Ingress.Spec.Backend = nil
113+
114+
// Add acceptable master annotations to minion
115+
mergeMasterAnnotationsIntoMinion(minion.Ingress.Annotations, mergeableIngs.Master.Ingress.Annotations)
116+
117+
removedAnnotations = filterMinionAnnotations(minion.Ingress.Annotations)
118+
if len(removedAnnotations) != 0 {
119+
glog.Errorf("Ingress Resource %v/%v with the annotation 'nginx.com/mergeable-ingress-type' set to 'minion' cannot contain the %v annotation(s). They will be ignored",
120+
minion.Ingress.Namespace, minion.Ingress.Name, strings.Join(removedAnnotations, ","))
121+
}
122+
104123
pems, jwtKeyFileName := cnf.updateSecrets(minion)
105124
nginxCfg := cnf.generateNginxCfg(minion, pems, jwtKeyFileName)
106125

107-
// Replace all minion annotations with master annotations
108-
minion.Ingress.Annotations = mergeableIngs.Master.Ingress.Annotations
109-
110126
for _, server := range nginxCfg.Servers {
111127
for _, loc := range server.Locations {
112-
if loc.Path != "/" {
113-
loc.IngressResource = objectMetaToFileName(&minion.Ingress.ObjectMeta)
114-
locations = append(locations, loc)
115-
}
128+
loc.IngressResource = objectMetaToFileName(&minion.Ingress.ObjectMeta)
129+
locations = append(locations, loc)
116130
}
117131
}
132+
118133
for _, val := range nginxCfg.Upstreams {
119134
upstreams = append(upstreams, val)
120135
}
@@ -766,6 +781,48 @@ func (cnf *Configurator) updatePlusEndpoints(ingEx *IngressEx) error {
766781
return nil
767782
}
768783

784+
func filterMasterAnnotations(annotations map[string]string) []string {
785+
var removedAnnotations []string
786+
787+
for _, blacklistAnn := range masterBlacklist {
788+
if _, ok := annotations[blacklistAnn]; ok {
789+
removedAnnotations = append(removedAnnotations, blacklistAnn)
790+
delete(annotations, blacklistAnn)
791+
}
792+
}
793+
794+
return removedAnnotations
795+
}
796+
797+
func filterMinionAnnotations(annotations map[string]string) []string {
798+
var removedAnnotations []string
799+
800+
for _, blacklistAnn := range minionBlacklist {
801+
if _, ok := annotations[blacklistAnn]; ok {
802+
removedAnnotations = append(removedAnnotations, blacklistAnn)
803+
delete(annotations, blacklistAnn)
804+
}
805+
}
806+
807+
return removedAnnotations
808+
}
809+
810+
func mergeMasterAnnotationsIntoMinion(minionAnnotations map[string]string, masterAnnotations map[string]string) {
811+
for key, val := range masterAnnotations {
812+
isBlacklisted := false
813+
if _, ok := minionAnnotations[key]; !ok {
814+
for _, blacklistAnn := range minionBlacklist {
815+
if blacklistAnn == key {
816+
isBlacklisted = true
817+
}
818+
}
819+
if !isBlacklisted {
820+
minionAnnotations[key] = val
821+
}
822+
}
823+
}
824+
}
825+
769826
// UpdateConfig updates NGINX Configuration parameters
770827
func (cnf *Configurator) UpdateConfig(config *Config, ingExes []*IngressEx, mergeableIngs map[string]*MergeableIngresses) error {
771828
cnf.config = config

nginx-controller/nginx/configurator_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package nginx
22

33
import (
4+
"reflect"
45
"testing"
56
)
67

@@ -61,3 +62,86 @@ func TestParseStickyServiceInvalidFormat(t *testing.T) {
6162
t.Errorf("parseStickyService(%s) should return error, got nil", stickyService)
6263
}
6364
}
65+
66+
func TestFilterMasterAnnotations(t *testing.T) {
67+
masterAnnotations := map[string]string{
68+
"nginx.org/rewrites": "serviceName=service1 rewrite=rewrite1",
69+
"nginx.org/ssl-services": "service1",
70+
"nginx.org/hsts": "True",
71+
"nginx.org/hsts-max-age": "2700000",
72+
"nginx.org/hsts-include-subdomains": "True",
73+
}
74+
removedAnnotations := filterMasterAnnotations(masterAnnotations)
75+
76+
expectedfilteredMasterAnnotations := map[string]string{
77+
"nginx.org/hsts": "True",
78+
"nginx.org/hsts-max-age": "2700000",
79+
"nginx.org/hsts-include-subdomains": "True",
80+
}
81+
expectedRemovedAnnotations := []string{
82+
"nginx.org/rewrites",
83+
"nginx.org/ssl-services",
84+
}
85+
86+
if !reflect.DeepEqual(expectedfilteredMasterAnnotations, masterAnnotations) {
87+
t.Errorf("filterMasterAnnotations returned %v, but expected %v", masterAnnotations, expectedfilteredMasterAnnotations)
88+
}
89+
if !reflect.DeepEqual(expectedRemovedAnnotations, removedAnnotations) {
90+
t.Errorf("filterMasterAnnotations returned %v, but expected %v", removedAnnotations, expectedRemovedAnnotations)
91+
}
92+
}
93+
94+
func TestFilterMinionAnnotations(t *testing.T) {
95+
minionAnnotations := map[string]string{
96+
"nginx.org/rewrites": "serviceName=service1 rewrite=rewrite1",
97+
"nginx.org/ssl-services": "service1",
98+
"nginx.org/hsts": "True",
99+
"nginx.org/hsts-max-age": "2700000",
100+
"nginx.org/hsts-include-subdomains": "True",
101+
}
102+
removedAnnotations := filterMinionAnnotations(minionAnnotations)
103+
104+
expectedfilteredMinionAnnotations := map[string]string{
105+
"nginx.org/rewrites": "serviceName=service1 rewrite=rewrite1",
106+
"nginx.org/ssl-services": "service1",
107+
}
108+
expectedRemovedAnnotations := []string{
109+
"nginx.org/hsts",
110+
"nginx.org/hsts-max-age",
111+
"nginx.org/hsts-include-subdomains",
112+
}
113+
114+
if !reflect.DeepEqual(expectedfilteredMinionAnnotations, minionAnnotations) {
115+
t.Errorf("filterMinionAnnotations returned %v, but expected %v", minionAnnotations, expectedfilteredMinionAnnotations)
116+
}
117+
if !reflect.DeepEqual(expectedRemovedAnnotations, removedAnnotations) {
118+
t.Errorf("filterMinionAnnotations returned %v, but expected %v", removedAnnotations, expectedRemovedAnnotations)
119+
}
120+
}
121+
122+
func TestMergeMasterAnnotationsIntoMinion(t *testing.T) {
123+
masterAnnotations := map[string]string{
124+
"nginx.org/proxy-buffering": "True",
125+
"nginx.org/proxy-buffers": "2",
126+
"nginx.org/proxy-buffer-size": "8k",
127+
"nginx.org/hsts": "True",
128+
"nginx.org/hsts-max-age": "2700000",
129+
"nginx.org/proxy-connect-timeout": "50s",
130+
}
131+
minionAnnotations := map[string]string{
132+
"nginx.org/client-max-body-size": "2m",
133+
"nginx.org/proxy-connect-timeout": "20s",
134+
}
135+
mergeMasterAnnotationsIntoMinion(minionAnnotations, masterAnnotations)
136+
137+
expectedMergedAnnotations := map[string]string{
138+
"nginx.org/proxy-buffering": "True",
139+
"nginx.org/proxy-buffers": "2",
140+
"nginx.org/proxy-buffer-size": "8k",
141+
"nginx.org/client-max-body-size": "2m",
142+
"nginx.org/proxy-connect-timeout": "20s",
143+
}
144+
if !reflect.DeepEqual(expectedMergedAnnotations, minionAnnotations) {
145+
t.Errorf("mergeMasterAnnotationsIntoMinion returned %v, but expected %v", minionAnnotations, expectedMergedAnnotations)
146+
}
147+
}

nginx-controller/nginx/ingress.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ type IngressEx struct {
1515
}
1616

1717
type MergeableIngresses struct {
18-
Master *IngressEx
18+
Master *IngressEx
1919
Minions []*IngressEx
2020
}
21+
22+
var masterBlacklist = []string{
23+
"nginx.org/rewrites",
24+
"nginx.org/ssl-services",
25+
"nginx.org/websocket-services",
26+
"nginx.com/sticky-cookie-services",
27+
}
28+
29+
var minionBlacklist = []string{
30+
"nginx.org/proxy-hide-headers",
31+
"nginx.org/proxy-pass-headers",
32+
"nginx.org/redirect-to-https",
33+
"ingress.kubernetes.io/ssl-redirect",
34+
"nginx.org/hsts",
35+
"nginx.org/hsts-max-age",
36+
"nginx.org/hsts-include-subdomains",
37+
"nginx.org/server-tokens",
38+
"nginx.org/listen-ports",
39+
"nginx.org/listen-ports-ssl",
40+
"nginx.com/jwt-key",
41+
"nginx.com/jwt-realm",
42+
"nginx.com/jwt-token",
43+
"nginx.com/jwt-login-url",
44+
}

0 commit comments

Comments
 (0)