Skip to content

Commit cace410

Browse files
authored
Add support for path-regex annotation in Ingress Master-Minion (#4200)
1 parent 514b275 commit cace410

File tree

6 files changed

+1340
-522
lines changed

6 files changed

+1340
-522
lines changed

internal/configs/version1/nginx-plus.ingress.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ server {
177177
{{end -}}
178178

179179
{{range $location := $server.Locations}}
180-
location {{ makeLocationPath $location.Path $.Ingress.Annotations | printf }} {
180+
location {{ makeLocationPath $location $.Ingress.Annotations | printf }} {
181181
set $service "{{$location.ServiceName}}";
182182
status_zone "{{ $location.ServiceName }}";
183183
{{with $location.MinionIngress}}

internal/configs/version1/nginx.ingress.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ server {
102102
{{- end}}
103103

104104
{{range $location := $server.Locations}}
105-
location {{ makeLocationPath $location.Path $.Ingress.Annotations | printf }} {
105+
location {{ makeLocationPath $location $.Ingress.Annotations | printf }} {
106106
set $service "{{$location.ServiceName}}";
107107
{{with $location.MinionIngress}}
108108
# location for minion {{$location.MinionIngress.Namespace}}/{{$location.MinionIngress.Name}}

internal/configs/version1/template_helper.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,42 @@ func trim(s string) string {
1414
return strings.TrimSpace(s)
1515
}
1616

17-
// makeLocationPath takes a string representing a location path
18-
// and a map representing Ingress annotations.
17+
// makeLocationPath takes location and Ingress annotations and returns
18+
// modified location path with added regex modifier or the original path
19+
// if no path-regex annotation is present in ingressAnnotations
20+
// or in Location's Ingress.
21+
//
22+
// Annotation 'path-regex' set on a Minion Ingress will take priority over
23+
// the annotation set on the master (in Master-Minion Ingress setup).
24+
// If no annotation 'path-regex' is set on Minion and only on Ingress (including master),
25+
// all location paths will be updated using provided regex modifier types.
26+
func makeLocationPath(loc *Location, ingressAnnotations map[string]string) string {
27+
if loc.MinionIngress != nil {
28+
// Case when annotation 'path-regex' set on Location's Minion.
29+
_, isMinion := loc.MinionIngress.Annotations["nginx.org/mergeable-ingress-type"]
30+
regexType, hasRegex := loc.MinionIngress.Annotations["nginx.org/path-regex"]
31+
32+
if isMinion && hasRegex {
33+
return makePathWithRegex(loc.Path, regexType)
34+
}
35+
}
36+
37+
// Case when annotation 'path-regex' set on Ingress (including Master).
38+
regexType, ok := ingressAnnotations["nginx.org/path-regex"]
39+
if !ok {
40+
return loc.Path
41+
}
42+
return makePathWithRegex(loc.Path, regexType)
43+
}
44+
45+
// makePathWithRegex takes a path representing a location and a regexType
46+
// (one of `case_sensitive`, `case_insensitive` or `exact`).
1947
// It returns a location path with added regular expression modifier.
2048
// See [Location Directive].
2149
//
2250
// [Location Directive]: https://nginx.org/en/docs/http/ngx_http_core_module.html#location
23-
func makeLocationPath(path string, annotations map[string]string) string {
24-
p, ok := annotations["nginx.org/path-regex"]
25-
if !ok {
26-
return path
27-
}
28-
switch p {
51+
func makePathWithRegex(path, regexType string) string {
52+
switch regexType {
2953
case "case_sensitive":
3054
return fmt.Sprintf("~ \"^%s\"", path)
3155
case "case_insensitive":

internal/configs/version1/template_helper_test.go

Lines changed: 196 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,110 +6,273 @@ import (
66
"text/template"
77
)
88

9-
func TestWithPathRegex_MatchesCaseSensitiveModifier(t *testing.T) {
9+
func TestMakeLocationPath_WithRegexCaseSensitiveModifier(t *testing.T) {
1010
t.Parallel()
1111

1212
want := "~ \"^/coffee/[A-Z0-9]{3}\""
13-
got := makeLocationPath("/coffee/[A-Z0-9]{3}", map[string]string{"nginx.org/path-regex": "case_sensitive"})
13+
got := makeLocationPath(
14+
&Location{Path: "/coffee/[A-Z0-9]{3}"},
15+
map[string]string{"nginx.org/path-regex": "case_sensitive"},
16+
)
1417
if got != want {
1518
t.Errorf("got: %s, want: %s", got, want)
1619
}
1720
}
1821

19-
func TestWithPathRegex_MatchesCaseInsensitiveModifier(t *testing.T) {
22+
func TestMakeLocationPath_WithRegexCaseInsensitiveModifier(t *testing.T) {
2023
t.Parallel()
2124

2225
want := "~* \"^/coffee/[A-Z0-9]{3}\""
23-
got := makeLocationPath("/coffee/[A-Z0-9]{3}", map[string]string{"nginx.org/path-regex": "case_insensitive"})
26+
got := makeLocationPath(
27+
&Location{Path: "/coffee/[A-Z0-9]{3}"},
28+
map[string]string{"nginx.org/path-regex": "case_insensitive"},
29+
)
2430
if got != want {
2531
t.Errorf("got: %s, want: %s", got, want)
2632
}
2733
}
2834

29-
func TestWithPathReqex_MatchesExactModifier(t *testing.T) {
35+
func TestMakeLocationPath_WithRegexExactModifier(t *testing.T) {
3036
t.Parallel()
3137

3238
want := "= \"/coffee\""
33-
got := makeLocationPath("/coffee", map[string]string{"nginx.org/path-regex": "exact"})
39+
got := makeLocationPath(
40+
&Location{Path: "/coffee"},
41+
map[string]string{"nginx.org/path-regex": "exact"},
42+
)
3443
if got != want {
3544
t.Errorf("got: %s, want: %s", got, want)
3645
}
3746
}
3847

39-
func TestWithPathReqex_DoesNotMatchModifier(t *testing.T) {
48+
func TestMakeLocationPath_WithBogusRegexModifier(t *testing.T) {
4049
t.Parallel()
4150

4251
want := "/coffee"
43-
got := makeLocationPath("/coffee", map[string]string{"nginx.org/path-regex": "bogus"})
52+
got := makeLocationPath(
53+
&Location{Path: "/coffee"},
54+
map[string]string{"nginx.org/path-regex": "bogus"},
55+
)
4456
if got != want {
4557
t.Errorf("got: %s, want: %s", got, want)
4658
}
4759
}
4860

49-
func TestWithPathReqex_DoesNotMatchEmptyModifier(t *testing.T) {
61+
func TestMakeLocationPath_WithEmptyRegexModifier(t *testing.T) {
5062
t.Parallel()
5163

5264
want := "/coffee"
53-
got := makeLocationPath("/coffee", map[string]string{"nginx.org/path-regex": ""})
65+
got := makeLocationPath(
66+
&Location{Path: "/coffee"},
67+
map[string]string{"nginx.org/path-regex": ""},
68+
)
5469
if got != want {
5570
t.Errorf("got: %s, want: %s", got, want)
5671
}
5772
}
5873

59-
func TestWithPathReqex_DoesNotMatchBogusAnnotationName(t *testing.T) {
74+
func TestMakeLocationPath_WithBogusAnnotationName(t *testing.T) {
6075
t.Parallel()
6176

6277
want := "/coffee"
63-
got := makeLocationPath("/coffee", map[string]string{"nginx.org/bogus-annotation": ""})
78+
got := makeLocationPath(
79+
&Location{Path: "/coffee"},
80+
map[string]string{"nginx.org/bogus-annotation": ""},
81+
)
6482
if got != want {
6583
t.Errorf("got: %s, want: %s", got, want)
6684
}
6785
}
6886

69-
func TestSplitHelperFunction(t *testing.T) {
87+
func TestMakeLocationPath_ForIngressWithoutPathRegex(t *testing.T) {
7088
t.Parallel()
71-
const tpl = `{{range $n := split . ","}}{{$n}} {{end}}`
7289

73-
tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(tpl)
74-
if err != nil {
75-
t.Fatalf("Failed to parse template: %v", err)
90+
want := "/coffee"
91+
got := makeLocationPath(
92+
&Location{Path: "/coffee"},
93+
map[string]string{},
94+
)
95+
if got != want {
96+
t.Errorf("got %q, want %q", got, want)
97+
}
98+
}
99+
100+
func TestMakeLocationPath_ForIngressWithPathRegexCaseSensitive(t *testing.T) {
101+
t.Parallel()
102+
103+
want := "~ \"^/coffee\""
104+
got := makeLocationPath(
105+
&Location{Path: "/coffee"},
106+
map[string]string{
107+
"nginx.org/path-regex": "case_sensitive",
108+
},
109+
)
110+
if got != want {
111+
t.Errorf("got %q, want %q", got, want)
112+
}
113+
}
114+
115+
func TestMakeLocationPath_ForIngressWithPathRegexSetOnMinion(t *testing.T) {
116+
t.Parallel()
117+
118+
want := "~ \"^/coffee\""
119+
got := makeLocationPath(
120+
&Location{
121+
Path: "/coffee",
122+
MinionIngress: &Ingress{
123+
Name: "cafe-ingress-coffee-minion",
124+
Namespace: "default",
125+
Annotations: map[string]string{
126+
"nginx.org/mergeable-ingress-type": "minion",
127+
"nginx.org/path-regex": "case_sensitive",
128+
},
129+
},
130+
},
131+
map[string]string{
132+
"nginx.org/mergeable-ingress-type": "master",
133+
},
134+
)
135+
136+
if got != want {
137+
t.Errorf("got %q, want %q", got, want)
138+
}
139+
}
140+
141+
func TestMakeLocationPath_ForIngressWithPathRegexSetOnMaster(t *testing.T) {
142+
t.Parallel()
143+
144+
want := "~ \"^/coffee\""
145+
got := makeLocationPath(
146+
&Location{
147+
Path: "/coffee",
148+
MinionIngress: &Ingress{
149+
Name: "cafe-ingress-coffee-minion",
150+
Namespace: "default",
151+
Annotations: map[string]string{
152+
"nginx.org/mergeable-ingress-type": "minion",
153+
},
154+
},
155+
},
156+
map[string]string{
157+
"nginx.org/mergeable-ingress-type": "master",
158+
"nginx.org/path-regex": "case_sensitive",
159+
},
160+
)
161+
162+
if got != want {
163+
t.Errorf("got %q, want %q", got, want)
164+
}
165+
}
166+
167+
func TestMakeLocationPath_SetOnMinionTakesPrecedenceOverMaster(t *testing.T) {
168+
t.Parallel()
169+
170+
want := "= \"/coffee\""
171+
got := makeLocationPath(
172+
&Location{
173+
Path: "/coffee",
174+
MinionIngress: &Ingress{
175+
Name: "cafe-ingress-coffee-minion",
176+
Namespace: "default",
177+
Annotations: map[string]string{
178+
"nginx.org/mergeable-ingress-type": "minion",
179+
"nginx.org/path-regex": "exact",
180+
},
181+
},
182+
},
183+
map[string]string{
184+
"nginx.org/mergeable-ingress-type": "master",
185+
"nginx.org/path-regex": "case_sensitive",
186+
},
187+
)
188+
189+
if got != want {
190+
t.Errorf("got %q, want %q", got, want)
76191
}
192+
}
193+
194+
func TestMakeLocationPath_PathRegexSetOnMaster(t *testing.T) {
195+
t.Parallel()
196+
197+
want := "= \"/coffee\""
198+
got := makeLocationPath(
199+
&Location{
200+
Path: "/coffee",
201+
MinionIngress: &Ingress{
202+
Name: "cafe-ingress-coffee-minion",
203+
Namespace: "default",
204+
Annotations: map[string]string{
205+
"nginx.org/mergeable-ingress-type": "minion",
206+
},
207+
},
208+
},
209+
map[string]string{
210+
"nginx.org/mergeable-ingress-type": "master",
211+
"nginx.org/path-regex": "exact",
212+
},
213+
)
77214

215+
if got != want {
216+
t.Errorf("got %q, want %q", got, want)
217+
}
218+
}
219+
220+
func TestSplitInputString(t *testing.T) {
221+
t.Parallel()
222+
223+
tmpl := newSplitTemplate(t)
78224
var buf bytes.Buffer
79225

80226
input := "foo,bar"
81227
expected := "foo bar "
82228

83-
err = tmpl.Execute(&buf, input)
229+
err := tmpl.Execute(&buf, input)
84230
if err != nil {
85231
t.Fatalf("Failed to execute the template %v", err)
86232
}
87-
88233
if buf.String() != expected {
89-
t.Fatalf("Template generated wrong config, got %v but expected %v.", buf.String(), expected)
234+
t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), expected)
90235
}
91236
}
92237

93-
func TestTrimHelperFunction(t *testing.T) {
238+
func TestTrimWhiteSpaceFromInputString(t *testing.T) {
94239
t.Parallel()
95-
const tpl = `{{trim .}}`
96240

97-
tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(tpl)
98-
if err != nil {
99-
t.Fatalf("Failed to parse template: %v", err)
241+
tmpl := newTrimTemplate(t)
242+
inputs := []string{
243+
" foobar ",
244+
"foobar ",
245+
" foobar",
246+
"foobar",
100247
}
101-
102-
var buf bytes.Buffer
103-
104-
input := " foobar "
105248
expected := "foobar"
106249

107-
err = tmpl.Execute(&buf, input)
250+
for _, i := range inputs {
251+
var buf bytes.Buffer
252+
err := tmpl.Execute(&buf, i)
253+
if err != nil {
254+
t.Fatalf("Failed to execute the template %v", err)
255+
}
256+
if buf.String() != expected {
257+
t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), expected)
258+
}
259+
}
260+
}
261+
262+
func newSplitTemplate(t *testing.T) *template.Template {
263+
t.Helper()
264+
tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{range $n := split . ","}}{{$n}} {{end}}`)
108265
if err != nil {
109-
t.Fatalf("Failed to execute the template %v", err)
266+
t.Fatalf("Failed to parse template: %v", err)
110267
}
268+
return tmpl
269+
}
111270

112-
if buf.String() != expected {
113-
t.Fatalf("Template generated wrong config, got %v but expected %v.", buf.String(), expected)
271+
func newTrimTemplate(t *testing.T) *template.Template {
272+
t.Helper()
273+
tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{trim .}}`)
274+
if err != nil {
275+
t.Fatalf("Failed to parse template: %v", err)
114276
}
277+
return tmpl
115278
}

0 commit comments

Comments
 (0)