From cace41083fbe9eec57d7d52d0a263e56f34b65da Mon Sep 17 00:00:00 2001 From: Jakub Jarosz <99677300+jjngx@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:02:58 +0100 Subject: [PATCH] Add support for path-regex annotation in Ingress Master-Minion (#4200) --- .../configs/version1/nginx-plus.ingress.tmpl | 2 +- internal/configs/version1/nginx.ingress.tmpl | 2 +- internal/configs/version1/template_helper.go | 40 +- .../configs/version1/template_helper_test.go | 229 +++- internal/configs/version1/template_test.go | 1110 +++++++++++++++++ internal/configs/version1/templates_test.go | 479 ------- 6 files changed, 1340 insertions(+), 522 deletions(-) create mode 100644 internal/configs/version1/template_test.go delete mode 100644 internal/configs/version1/templates_test.go diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index a2af55811f..972f4dbdd3 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -177,7 +177,7 @@ server { {{end -}} {{range $location := $server.Locations}} - location {{ makeLocationPath $location.Path $.Ingress.Annotations | printf }} { + location {{ makeLocationPath $location $.Ingress.Annotations | printf }} { set $service "{{$location.ServiceName}}"; status_zone "{{ $location.ServiceName }}"; {{with $location.MinionIngress}} diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index 62d673e284..6d1c566020 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -102,7 +102,7 @@ server { {{- end}} {{range $location := $server.Locations}} - location {{ makeLocationPath $location.Path $.Ingress.Annotations | printf }} { + location {{ makeLocationPath $location $.Ingress.Annotations | printf }} { set $service "{{$location.ServiceName}}"; {{with $location.MinionIngress}} # location for minion {{$location.MinionIngress.Namespace}}/{{$location.MinionIngress.Name}} diff --git a/internal/configs/version1/template_helper.go b/internal/configs/version1/template_helper.go index 0938181e60..dbb7125a92 100644 --- a/internal/configs/version1/template_helper.go +++ b/internal/configs/version1/template_helper.go @@ -14,18 +14,42 @@ func trim(s string) string { return strings.TrimSpace(s) } -// makeLocationPath takes a string representing a location path -// and a map representing Ingress annotations. +// makeLocationPath takes location and Ingress annotations and returns +// modified location path with added regex modifier or the original path +// if no path-regex annotation is present in ingressAnnotations +// or in Location's Ingress. +// +// Annotation 'path-regex' set on a Minion Ingress will take priority over +// the annotation set on the master (in Master-Minion Ingress setup). +// If no annotation 'path-regex' is set on Minion and only on Ingress (including master), +// all location paths will be updated using provided regex modifier types. +func makeLocationPath(loc *Location, ingressAnnotations map[string]string) string { + if loc.MinionIngress != nil { + // Case when annotation 'path-regex' set on Location's Minion. + _, isMinion := loc.MinionIngress.Annotations["nginx.org/mergeable-ingress-type"] + regexType, hasRegex := loc.MinionIngress.Annotations["nginx.org/path-regex"] + + if isMinion && hasRegex { + return makePathWithRegex(loc.Path, regexType) + } + } + + // Case when annotation 'path-regex' set on Ingress (including Master). + regexType, ok := ingressAnnotations["nginx.org/path-regex"] + if !ok { + return loc.Path + } + return makePathWithRegex(loc.Path, regexType) +} + +// makePathWithRegex takes a path representing a location and a regexType +// (one of `case_sensitive`, `case_insensitive` or `exact`). // It returns a location path with added regular expression modifier. // See [Location Directive]. // // [Location Directive]: https://nginx.org/en/docs/http/ngx_http_core_module.html#location -func makeLocationPath(path string, annotations map[string]string) string { - p, ok := annotations["nginx.org/path-regex"] - if !ok { - return path - } - switch p { +func makePathWithRegex(path, regexType string) string { + switch regexType { case "case_sensitive": return fmt.Sprintf("~ \"^%s\"", path) case "case_insensitive": diff --git a/internal/configs/version1/template_helper_test.go b/internal/configs/version1/template_helper_test.go index 82d8d9379f..3df1887401 100644 --- a/internal/configs/version1/template_helper_test.go +++ b/internal/configs/version1/template_helper_test.go @@ -6,110 +6,273 @@ import ( "text/template" ) -func TestWithPathRegex_MatchesCaseSensitiveModifier(t *testing.T) { +func TestMakeLocationPath_WithRegexCaseSensitiveModifier(t *testing.T) { t.Parallel() want := "~ \"^/coffee/[A-Z0-9]{3}\"" - got := makeLocationPath("/coffee/[A-Z0-9]{3}", map[string]string{"nginx.org/path-regex": "case_sensitive"}) + got := makeLocationPath( + &Location{Path: "/coffee/[A-Z0-9]{3}"}, + map[string]string{"nginx.org/path-regex": "case_sensitive"}, + ) if got != want { t.Errorf("got: %s, want: %s", got, want) } } -func TestWithPathRegex_MatchesCaseInsensitiveModifier(t *testing.T) { +func TestMakeLocationPath_WithRegexCaseInsensitiveModifier(t *testing.T) { t.Parallel() want := "~* \"^/coffee/[A-Z0-9]{3}\"" - got := makeLocationPath("/coffee/[A-Z0-9]{3}", map[string]string{"nginx.org/path-regex": "case_insensitive"}) + got := makeLocationPath( + &Location{Path: "/coffee/[A-Z0-9]{3}"}, + map[string]string{"nginx.org/path-regex": "case_insensitive"}, + ) if got != want { t.Errorf("got: %s, want: %s", got, want) } } -func TestWithPathReqex_MatchesExactModifier(t *testing.T) { +func TestMakeLocationPath_WithRegexExactModifier(t *testing.T) { t.Parallel() want := "= \"/coffee\"" - got := makeLocationPath("/coffee", map[string]string{"nginx.org/path-regex": "exact"}) + got := makeLocationPath( + &Location{Path: "/coffee"}, + map[string]string{"nginx.org/path-regex": "exact"}, + ) if got != want { t.Errorf("got: %s, want: %s", got, want) } } -func TestWithPathReqex_DoesNotMatchModifier(t *testing.T) { +func TestMakeLocationPath_WithBogusRegexModifier(t *testing.T) { t.Parallel() want := "/coffee" - got := makeLocationPath("/coffee", map[string]string{"nginx.org/path-regex": "bogus"}) + got := makeLocationPath( + &Location{Path: "/coffee"}, + map[string]string{"nginx.org/path-regex": "bogus"}, + ) if got != want { t.Errorf("got: %s, want: %s", got, want) } } -func TestWithPathReqex_DoesNotMatchEmptyModifier(t *testing.T) { +func TestMakeLocationPath_WithEmptyRegexModifier(t *testing.T) { t.Parallel() want := "/coffee" - got := makeLocationPath("/coffee", map[string]string{"nginx.org/path-regex": ""}) + got := makeLocationPath( + &Location{Path: "/coffee"}, + map[string]string{"nginx.org/path-regex": ""}, + ) if got != want { t.Errorf("got: %s, want: %s", got, want) } } -func TestWithPathReqex_DoesNotMatchBogusAnnotationName(t *testing.T) { +func TestMakeLocationPath_WithBogusAnnotationName(t *testing.T) { t.Parallel() want := "/coffee" - got := makeLocationPath("/coffee", map[string]string{"nginx.org/bogus-annotation": ""}) + got := makeLocationPath( + &Location{Path: "/coffee"}, + map[string]string{"nginx.org/bogus-annotation": ""}, + ) if got != want { t.Errorf("got: %s, want: %s", got, want) } } -func TestSplitHelperFunction(t *testing.T) { +func TestMakeLocationPath_ForIngressWithoutPathRegex(t *testing.T) { t.Parallel() - const tpl = `{{range $n := split . ","}}{{$n}} {{end}}` - tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(tpl) - if err != nil { - t.Fatalf("Failed to parse template: %v", err) + want := "/coffee" + got := makeLocationPath( + &Location{Path: "/coffee"}, + map[string]string{}, + ) + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestMakeLocationPath_ForIngressWithPathRegexCaseSensitive(t *testing.T) { + t.Parallel() + + want := "~ \"^/coffee\"" + got := makeLocationPath( + &Location{Path: "/coffee"}, + map[string]string{ + "nginx.org/path-regex": "case_sensitive", + }, + ) + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestMakeLocationPath_ForIngressWithPathRegexSetOnMinion(t *testing.T) { + t.Parallel() + + want := "~ \"^/coffee\"" + got := makeLocationPath( + &Location{ + Path: "/coffee", + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_sensitive", + }, + }, + }, + map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + ) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestMakeLocationPath_ForIngressWithPathRegexSetOnMaster(t *testing.T) { + t.Parallel() + + want := "~ \"^/coffee\"" + got := makeLocationPath( + &Location{ + Path: "/coffee", + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + }, + map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + "nginx.org/path-regex": "case_sensitive", + }, + ) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestMakeLocationPath_SetOnMinionTakesPrecedenceOverMaster(t *testing.T) { + t.Parallel() + + want := "= \"/coffee\"" + got := makeLocationPath( + &Location{ + Path: "/coffee", + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "exact", + }, + }, + }, + map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + "nginx.org/path-regex": "case_sensitive", + }, + ) + + if got != want { + t.Errorf("got %q, want %q", got, want) } +} + +func TestMakeLocationPath_PathRegexSetOnMaster(t *testing.T) { + t.Parallel() + + want := "= \"/coffee\"" + got := makeLocationPath( + &Location{ + Path: "/coffee", + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + }, + map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + "nginx.org/path-regex": "exact", + }, + ) + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestSplitInputString(t *testing.T) { + t.Parallel() + + tmpl := newSplitTemplate(t) var buf bytes.Buffer input := "foo,bar" expected := "foo bar " - err = tmpl.Execute(&buf, input) + err := tmpl.Execute(&buf, input) if err != nil { t.Fatalf("Failed to execute the template %v", err) } - if buf.String() != expected { - t.Fatalf("Template generated wrong config, got %v but expected %v.", buf.String(), expected) + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), expected) } } -func TestTrimHelperFunction(t *testing.T) { +func TestTrimWhiteSpaceFromInputString(t *testing.T) { t.Parallel() - const tpl = `{{trim .}}` - tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(tpl) - if err != nil { - t.Fatalf("Failed to parse template: %v", err) + tmpl := newTrimTemplate(t) + inputs := []string{ + " foobar ", + "foobar ", + " foobar", + "foobar", } - - var buf bytes.Buffer - - input := " foobar " expected := "foobar" - err = tmpl.Execute(&buf, input) + for _, i := range inputs { + var buf bytes.Buffer + err := tmpl.Execute(&buf, i) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), expected) + } + } +} + +func newSplitTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{range $n := split . ","}}{{$n}} {{end}}`) if err != nil { - t.Fatalf("Failed to execute the template %v", err) + t.Fatalf("Failed to parse template: %v", err) } + return tmpl +} - if buf.String() != expected { - t.Fatalf("Template generated wrong config, got %v but expected %v.", buf.String(), expected) +func newTrimTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{trim .}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) } + return tmpl } diff --git a/internal/configs/version1/template_test.go b/internal/configs/version1/template_test.go new file mode 100644 index 0000000000..02f29606eb --- /dev/null +++ b/internal/configs/version1/template_test.go @@ -0,0 +1,1110 @@ +package version1 + +import ( + "bytes" + "strings" + "testing" + "text/template" +) + +func TestExecuteMainTemplateForNGINXPlus(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusMainTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, mainCfg) + if err != nil { + t.Error(err) + } + t.Log(buf.String()) +} + +func TestExecuteMainTemplateForNGINX(t *testing.T) { + t.Parallel() + + tmpl := newNGINXMainTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, mainCfg) + if err != nil { + t.Error(err) + } + t.Log(buf.String()) +} + +func TestExecuteTemplate_ForIngressForNGINXPlus(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfg) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } +} + +func TestExecuteTemplate_ForIngressForNGINX(t *testing.T) { + t.Parallel() + + tmpl := newNGINXIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfg) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } +} + +func TestExecuteTemplate_ForIngressForNGINXPlusWithRegexAnnotationCaseSensitiveModifier(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationCaseSensitive) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + wantLocation := "~ \"^/tea/[A-Z0-9]{3}\"" + if !strings.Contains(buf.String(), wantLocation) { + t.Errorf("want %q in generated config", wantLocation) + } +} + +func TestExecuteTemplate_ForIngressForNGINXPlusWithRegexAnnotationCaseInsensitiveModifier(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationCaseInsensitive) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + wantLocation := "~* \"^/tea/[A-Z0-9]{3}\"" + if !strings.Contains(buf.String(), wantLocation) { + t.Errorf("want %q in generated config", wantLocation) + } +} + +func TestExecuteTemplate_ForIngressForNGINXPlusWithRegexAnnotationExactMatchModifier(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationExactMatch) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + wantLocation := "= \"/tea\"" + if !strings.Contains(buf.String(), wantLocation) { + t.Errorf("want %q in generated config", wantLocation) + } +} + +func TestExecuteTemplate_ForIngressForNGINXPlusWithRegexAnnotationEmpty(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationEmptyString) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + wantLocation := "/tea" + if !strings.Contains(buf.String(), wantLocation) { + t.Errorf("want %q in generated config", wantLocation) + } +} + +func TestExecuteTemplate_ForMergeableIngressForNGINXPlus(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgMasterMinionNGINXPlus) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + want := "location /coffee {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } + want = "location /tea {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } +} + +func TestExecuteTemplate_ForMergeableIngressWithOneMinionWithPathRegexAnnotation(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgMasterMinionNGINXPlusMinionWithPathRegexAnnotation) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + // Observe location /coffee updated with regex + want := "location ~* \"^/coffee\" {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } + // Observe location /tea not updated with regex + want = "location /tea {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } +} + +func TestExecuteTemplate_ForMergeableIngressWithSecondMinionWithPathRegexAnnotation(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgMasterMinionNGINXPlusSecondMinionWithPathRegexAnnotation) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + // Observe location /coffee not updated + want := "location /coffee {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } + // Observe location /tea updated with regex + want = "location ~ \"^/tea\" {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } +} + +func TestExecuteTemplate_ForMergeableIngressForNGINXPlusWithPathRegexAnnotationOnMaster(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgMasterMinionNGINXPlusMasterWithPathRegexAnnotation) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + want := "location ~ \"^/coffee\" {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } + want = "location ~ \"^/tea\" {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } +} + +func TestExecuteTemplate_ForMergeableIngressForNGINXPlusWithPathRegexAnnotationOnMasterAndMinions(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgMasterMinionNGINXPlusMasterAndAllMinionsWithPathRegexAnnotation) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + want := "location ~* \"^/coffee\"" + if !strings.Contains(buf.String(), want) { + t.Errorf("did not get %q in generated config", want) + } + want = "location ~* \"^/tea\"" + if !strings.Contains(buf.String(), want) { + t.Errorf("did not get %q in generated config", want) + } +} + +func TestExecuteTemplate_ForMergeableIngressForNGINXPlusWithPathRegexAnnotationOnMinionsNotOnMaster(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgMasterMinionNGINXPlusMasterWithoutPathRegexMinionsWithPathRegexAnnotation) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + want := "location ~* \"^/coffee\" {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } + want = "location ~ \"^/tea\" {" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } +} + +func newNGINXPlusIngressTmpl(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("nginx-plus.ingress.tmpl").Funcs(helperFunctions).ParseFiles("nginx-plus.ingress.tmpl") + if err != nil { + t.Fatal(err) + } + return tmpl +} + +func newNGINXIngressTmpl(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("nginx.ingress.tmpl").Funcs(helperFunctions).ParseFiles("nginx.ingress.tmpl") + if err != nil { + t.Fatal(err) + } + return tmpl +} + +func newNGINXPlusMainTmpl(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("nginx-plus.tmpl").ParseFiles("nginx-plus.tmpl") + if err != nil { + t.Fatal(err) + } + return tmpl +} + +func newNGINXMainTmpl(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("nginx.tmpl").ParseFiles("nginx.tmpl") + if err != nil { + t.Fatal(err) + } + return tmpl +} + +var ( + // Ingress Config example without added annotations + ingressCfg = IngressNginxConfig{ + Servers: []Server{ + { + Name: "test.example.com", + ServerTokens: "off", + StatusZone: "test.example.com", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + RedirectLocationName: "@login_url-default-cafe-ingress", + }, + SSL: true, + SSLCertificate: "secret.pem", + SSLCertificateKey: "secret.pem", + SSLPorts: []int{443}, + SSLRedirect: true, + Locations: []Location{ + { + Path: "/tea", + Upstream: testUpstream, + ProxyConnectTimeout: "10s", + ProxyReadTimeout: "10s", + ProxySendTimeout: "10s", + ClientMaxBodySize: "2m", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/location-key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + }, + MinionIngress: &Ingress{ + Name: "tea-minion", + Namespace: "default", + }, + }, + }, + HealthChecks: map[string]HealthCheck{"test": healthCheck}, + JWTRedirectLocations: []JWTRedirectLocation{ + { + Name: "@login_url-default-cafe-ingress", + LoginURL: "https://test.example.com/login", + }, + }, + }, + }, + Upstreams: []Upstream{testUpstream}, + Keepalive: "16", + Ingress: Ingress{ + Name: "cafe-ingress", + Namespace: "default", + }, + } + + // Ingress Config example with path-regex annotation value "case_sensitive" + ingressCfgWithRegExAnnotationCaseSensitive = IngressNginxConfig{ + Servers: []Server{ + { + Name: "test.example.com", + ServerTokens: "off", + StatusZone: "test.example.com", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + RedirectLocationName: "@login_url-default-cafe-ingress", + }, + SSL: true, + SSLCertificate: "secret.pem", + SSLCertificateKey: "secret.pem", + SSLPorts: []int{443}, + SSLRedirect: true, + Locations: []Location{ + { + Path: "/tea/[A-Z0-9]{3}", + Upstream: testUpstream, + ProxyConnectTimeout: "10s", + ProxyReadTimeout: "10s", + ProxySendTimeout: "10s", + ClientMaxBodySize: "2m", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/location-key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + }, + MinionIngress: &Ingress{ + Name: "tea-minion", + Namespace: "default", + }, + }, + }, + HealthChecks: map[string]HealthCheck{"test": healthCheck}, + JWTRedirectLocations: []JWTRedirectLocation{ + { + Name: "@login_url-default-cafe-ingress", + LoginURL: "https://test.example.com/login", + }, + }, + }, + }, + Upstreams: []Upstream{testUpstream}, + Keepalive: "16", + Ingress: Ingress{ + Name: "cafe-ingress", + Namespace: "default", + Annotations: map[string]string{"nginx.org/path-regex": "case_sensitive"}, + }, + } + + // Ingress Config example with path-regex annotation value "case_insensitive" + ingressCfgWithRegExAnnotationCaseInsensitive = IngressNginxConfig{ + Servers: []Server{ + { + Name: "test.example.com", + ServerTokens: "off", + StatusZone: "test.example.com", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + RedirectLocationName: "@login_url-default-cafe-ingress", + }, + SSL: true, + SSLCertificate: "secret.pem", + SSLCertificateKey: "secret.pem", + SSLPorts: []int{443}, + SSLRedirect: true, + Locations: []Location{ + { + Path: "/tea/[A-Z0-9]{3}", + Upstream: testUpstream, + ProxyConnectTimeout: "10s", + ProxyReadTimeout: "10s", + ProxySendTimeout: "10s", + ClientMaxBodySize: "2m", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/location-key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + }, + MinionIngress: &Ingress{ + Name: "tea-minion", + Namespace: "default", + }, + }, + }, + HealthChecks: map[string]HealthCheck{"test": healthCheck}, + JWTRedirectLocations: []JWTRedirectLocation{ + { + Name: "@login_url-default-cafe-ingress", + LoginURL: "https://test.example.com/login", + }, + }, + }, + }, + Upstreams: []Upstream{testUpstream}, + Keepalive: "16", + Ingress: Ingress{ + Name: "cafe-ingress", + Namespace: "default", + Annotations: map[string]string{"nginx.org/path-regex": "case_insensitive"}, + }, + } + + // Ingress Config example with path-regex annotation value "exact" + ingressCfgWithRegExAnnotationExactMatch = IngressNginxConfig{ + Servers: []Server{ + { + Name: "test.example.com", + ServerTokens: "off", + StatusZone: "test.example.com", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + RedirectLocationName: "@login_url-default-cafe-ingress", + }, + SSL: true, + SSLCertificate: "secret.pem", + SSLCertificateKey: "secret.pem", + SSLPorts: []int{443}, + SSLRedirect: true, + Locations: []Location{ + { + Path: "/tea", + Upstream: testUpstream, + ProxyConnectTimeout: "10s", + ProxyReadTimeout: "10s", + ProxySendTimeout: "10s", + ClientMaxBodySize: "2m", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/location-key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + }, + MinionIngress: &Ingress{ + Name: "tea-minion", + Namespace: "default", + }, + }, + }, + HealthChecks: map[string]HealthCheck{"test": healthCheck}, + JWTRedirectLocations: []JWTRedirectLocation{ + { + Name: "@login_url-default-cafe-ingress", + LoginURL: "https://test.example.com/login", + }, + }, + }, + }, + Upstreams: []Upstream{testUpstream}, + Keepalive: "16", + Ingress: Ingress{ + Name: "cafe-ingress", + Namespace: "default", + Annotations: map[string]string{"nginx.org/path-regex": "exact"}, + }, + } + + // Ingress Config example with path-regex annotation value of an empty string + ingressCfgWithRegExAnnotationEmptyString = IngressNginxConfig{ + Servers: []Server{ + { + Name: "test.example.com", + ServerTokens: "off", + StatusZone: "test.example.com", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + RedirectLocationName: "@login_url-default-cafe-ingress", + }, + SSL: true, + SSLCertificate: "secret.pem", + SSLCertificateKey: "secret.pem", + SSLPorts: []int{443}, + SSLRedirect: true, + Locations: []Location{ + { + Path: "/tea", + Upstream: testUpstream, + ProxyConnectTimeout: "10s", + ProxyReadTimeout: "10s", + ProxySendTimeout: "10s", + ClientMaxBodySize: "2m", + JWTAuth: &JWTAuth{ + Key: "/etc/nginx/secrets/location-key.jwk", + Realm: "closed site", + Token: "$cookie_auth_token", + }, + MinionIngress: &Ingress{ + Name: "tea-minion", + Namespace: "default", + }, + }, + }, + HealthChecks: map[string]HealthCheck{"test": healthCheck}, + JWTRedirectLocations: []JWTRedirectLocation{ + { + Name: "@login_url-default-cafe-ingress", + LoginURL: "https://test.example.com/login", + }, + }, + }, + }, + Upstreams: []Upstream{testUpstream}, + Keepalive: "16", + Ingress: Ingress{ + Name: "cafe-ingress", + Namespace: "default", + Annotations: map[string]string{"nginx.org/path-regex": ""}, + }, + } + + mainCfg = MainConfig{ + ServerNamesHashMaxSize: "512", + ServerTokens: "off", + WorkerProcesses: "auto", + WorkerCPUAffinity: "auto", + WorkerShutdownTimeout: "1m", + WorkerConnections: "1024", + WorkerRlimitNofile: "65536", + LogFormat: []string{"$remote_addr", "$remote_user"}, + LogFormatEscaping: "default", + StreamSnippets: []string{"# comment"}, + StreamLogFormat: []string{"$remote_addr", "$remote_user"}, + StreamLogFormatEscaping: "none", + ResolverAddresses: []string{"example.com", "127.0.0.1"}, + ResolverIPV6: false, + ResolverValid: "10s", + ResolverTimeout: "15s", + KeepaliveTimeout: "65s", + KeepaliveRequests: 100, + VariablesHashBucketSize: 256, + VariablesHashMaxSize: 1024, + TLSPassthrough: true, + } + + // Vars for Mergable Ingress Master - Minion tests + + coffeeUpstreamNginxPlus = Upstream{ + Name: "default-cafe-ingress-coffee-minion-cafe.example.com-coffee-svc-80", + LBMethod: "random two least_conn", + UpstreamZoneSize: "512k", + UpstreamServers: []UpstreamServer{ + { + Address: "10.0.0.1:80", + MaxFails: 1, + MaxConns: 0, + FailTimeout: "10s", + }, + }, + UpstreamLabels: UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "ingress", + ResourceName: "cafe-ingress-coffee-minion", + ResourceNamespace: "default", + }, + } + + teaUpstreamNGINXPlus = Upstream{ + Name: "default-cafe-ingress-tea-minion-cafe.example.com-tea-svc-80", + LBMethod: "random two least_conn", + UpstreamZoneSize: "512k", + UpstreamServers: []UpstreamServer{ + { + Address: "10.0.0.2:80", + MaxFails: 1, + MaxConns: 0, + FailTimeout: "10s", + }, + }, + UpstreamLabels: UpstreamLabels{ + Service: "tea-svc", + ResourceType: "ingress", + ResourceName: "cafe-ingress-tea-minion", + ResourceNamespace: "default", + }, + } + + ingressCfgMasterMinionNGINXPlus = IngressNginxConfig{ + Upstreams: []Upstream{ + coffeeUpstreamNginxPlus, + teaUpstreamNGINXPlus, + }, + Servers: []Server{ + { + Name: "cafe.example.com", + ServerTokens: "on", + Locations: []Location{ + { + Path: "/coffee", + ServiceName: "coffee-svc", + Upstream: coffeeUpstreamNginxPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + ProxySSLName: "coffee-svc.default.svc", + }, + { + Path: "/tea", + ServiceName: "tea-svc", + Upstream: teaUpstreamNGINXPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-tea-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + ProxySSLName: "tea-svc.default.svc", + }, + }, + SSL: true, + SSLCertificate: "/etc/nginx/secrets/default-cafe-secret", + SSLCertificateKey: "/etc/nginx/secrets/default-cafe-secret", + StatusZone: "cafe.example.com", + HSTSMaxAge: 2592000, + Ports: []int{80}, + SSLPorts: []int{443}, + SSLRedirect: true, + HealthChecks: make(map[string]HealthCheck), + }, + }, + Ingress: Ingress{ + Name: "cafe-ingress-master", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + }, + } + + // ingressCfgMasterMinionNGINXPlusMinionWithPathRegexAnnotation holds data to test the following scenario: + // + // Ingress Master - Minion + // - Master: without `path-regex` annotation + // - Minion 1 (cafe-ingress-coffee-minion): with `path-regex` annotation + // - Minion 2 (cafe-ingress-tea-minion): without `path-regex` annotation + ingressCfgMasterMinionNGINXPlusMinionWithPathRegexAnnotation = IngressNginxConfig{ + Upstreams: []Upstream{ + coffeeUpstreamNginxPlus, + teaUpstreamNGINXPlus, + }, + Servers: []Server{ + { + Name: "cafe.example.com", + ServerTokens: "on", + Locations: []Location{ + { + Path: "/coffee", + ServiceName: "coffee-svc", + Upstream: coffeeUpstreamNginxPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_insensitive", + }, + }, + ProxySSLName: "coffee-svc.default.svc", + }, + { + Path: "/tea", + ServiceName: "tea-svc", + Upstream: teaUpstreamNGINXPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-tea-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + ProxySSLName: "tea-svc.default.svc", + }, + }, + SSL: true, + SSLCertificate: "/etc/nginx/secrets/default-cafe-secret", + SSLCertificateKey: "/etc/nginx/secrets/default-cafe-secret", + StatusZone: "cafe.example.com", + HSTSMaxAge: 2592000, + Ports: []int{80}, + SSLPorts: []int{443}, + SSLRedirect: true, + HealthChecks: make(map[string]HealthCheck), + }, + }, + Ingress: Ingress{ + Name: "cafe-ingress-master", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + }, + } + + // ingressCfgMasterMinionNGINXPlusSecondMinionWithPathRegexAnnotation holds data to test the following scenario: + // + // Ingress Master - Minion + // - Master: without `path-regex` annotation + // - Minion 1 (cafe-ingress-coffee-minion): without `path-regex` annotation + // - Minion 2 (cafe-ingress-tea-minion): with `path-regex` annotation + ingressCfgMasterMinionNGINXPlusSecondMinionWithPathRegexAnnotation = IngressNginxConfig{ + Upstreams: []Upstream{ + coffeeUpstreamNginxPlus, + teaUpstreamNGINXPlus, + }, + Servers: []Server{ + { + Name: "cafe.example.com", + ServerTokens: "on", + Locations: []Location{ + { + Path: "/coffee", + ServiceName: "coffee-svc", + Upstream: coffeeUpstreamNginxPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + ProxySSLName: "coffee-svc.default.svc", + }, + { + Path: "/tea", + ServiceName: "tea-svc", + Upstream: teaUpstreamNGINXPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-tea-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_sensitive", + }, + }, + ProxySSLName: "tea-svc.default.svc", + }, + }, + SSL: true, + SSLCertificate: "/etc/nginx/secrets/default-cafe-secret", + SSLCertificateKey: "/etc/nginx/secrets/default-cafe-secret", + StatusZone: "cafe.example.com", + HSTSMaxAge: 2592000, + Ports: []int{80}, + SSLPorts: []int{443}, + SSLRedirect: true, + HealthChecks: make(map[string]HealthCheck), + }, + }, + Ingress: Ingress{ + Name: "cafe-ingress-master", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + }, + } + + // ingressCfgMasterMinionNGINXPlusMasterWithPathRegexAnnotation holds data to test the following scenario: + // + // Ingress Master - Minion + // + // - Master: with `path-regex` annotation + // - Minion 1 (cafe-ingress-coffee-minion): without `path-regex` annotation + // - Minion 2 (cafe-ingress-tea-minion): without `path-regex` annotation + ingressCfgMasterMinionNGINXPlusMasterWithPathRegexAnnotation = IngressNginxConfig{ + Upstreams: []Upstream{ + coffeeUpstreamNginxPlus, + teaUpstreamNGINXPlus, + }, + Servers: []Server{ + { + Name: "cafe.example.com", + ServerTokens: "on", + Locations: []Location{ + { + Path: "/coffee", + ServiceName: "coffee-svc", + Upstream: coffeeUpstreamNginxPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + ProxySSLName: "coffee-svc.default.svc", + }, + { + Path: "/tea", + ServiceName: "tea-svc", + Upstream: teaUpstreamNGINXPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-tea-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + }, + }, + ProxySSLName: "tea-svc.default.svc", + }, + }, + SSL: true, + SSLCertificate: "/etc/nginx/secrets/default-cafe-secret", + SSLCertificateKey: "/etc/nginx/secrets/default-cafe-secret", + StatusZone: "cafe.example.com", + HSTSMaxAge: 2592000, + Ports: []int{80}, + SSLPorts: []int{443}, + SSLRedirect: true, + HealthChecks: make(map[string]HealthCheck), + }, + }, + Ingress: Ingress{ + Name: "cafe-ingress-master", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + "nginx.org/path-regex": "case_sensitive", + }, + }, + } + + // ingressCfgMasterMinionNGINXPlusMasterAndAllMinionsWithPathRegexAnnotation holds data to test the following scenario: + // + // Ingress Master - Minion + // + // - Master: with `path-regex` annotation + // - Minion 1 (cafe-ingress-coffee-minion): with `path-regex` annotation + // - Minion 2 (cafe-ingress-tea-minion): with `path-regex` annotation + ingressCfgMasterMinionNGINXPlusMasterAndAllMinionsWithPathRegexAnnotation = IngressNginxConfig{ + Upstreams: []Upstream{ + coffeeUpstreamNginxPlus, + teaUpstreamNGINXPlus, + }, + Servers: []Server{ + { + Name: "cafe.example.com", + ServerTokens: "on", + Locations: []Location{ + { + Path: "/coffee", + ServiceName: "coffee-svc", + Upstream: coffeeUpstreamNginxPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_insensitive", + }, + }, + ProxySSLName: "coffee-svc.default.svc", + }, + { + Path: "/tea", + ServiceName: "tea-svc", + Upstream: teaUpstreamNGINXPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-tea-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_insensitive", + }, + }, + ProxySSLName: "tea-svc.default.svc", + }, + }, + SSL: true, + SSLCertificate: "/etc/nginx/secrets/default-cafe-secret", + SSLCertificateKey: "/etc/nginx/secrets/default-cafe-secret", + StatusZone: "cafe.example.com", + HSTSMaxAge: 2592000, + Ports: []int{80}, + SSLPorts: []int{443}, + SSLRedirect: true, + HealthChecks: make(map[string]HealthCheck), + }, + }, + Ingress: Ingress{ + Name: "cafe-ingress-master", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + "nginx.org/path-regex": "case_sensitive", + }, + }, + } + + // ingressCfgMasterMinionNGINXPlusMasterWithoutPathRegexMinionsWithPathRegexAnnotation holds data to test the following scenario: + // + // Ingress Master - Minion + // - Master: without `path-regex` annotation + // - Minion 1 (cafe-ingress-coffee-minion): with `path-regex` annotation + // - Minion 2 (cafe-ingress-tea-minion): with `path-regex` annotation + ingressCfgMasterMinionNGINXPlusMasterWithoutPathRegexMinionsWithPathRegexAnnotation = IngressNginxConfig{ + Upstreams: []Upstream{ + coffeeUpstreamNginxPlus, + teaUpstreamNGINXPlus, + }, + Servers: []Server{ + { + Name: "cafe.example.com", + ServerTokens: "on", + Locations: []Location{ + { + Path: "/coffee", + ServiceName: "coffee-svc", + Upstream: coffeeUpstreamNginxPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-coffee-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_insensitive", + }, + }, + ProxySSLName: "coffee-svc.default.svc", + }, + { + Path: "/tea", + ServiceName: "tea-svc", + Upstream: teaUpstreamNGINXPlus, + ProxyConnectTimeout: "60s", + ProxyReadTimeout: "60s", + ProxySendTimeout: "60s", + ClientMaxBodySize: "1m", + ProxyBuffering: true, + MinionIngress: &Ingress{ + Name: "cafe-ingress-tea-minion", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "minion", + "nginx.org/path-regex": "case_sensitive", + }, + }, + ProxySSLName: "tea-svc.default.svc", + }, + }, + SSL: true, + SSLCertificate: "/etc/nginx/secrets/default-cafe-secret", + SSLCertificateKey: "/etc/nginx/secrets/default-cafe-secret", + StatusZone: "cafe.example.com", + HSTSMaxAge: 2592000, + Ports: []int{80}, + SSLPorts: []int{443}, + SSLRedirect: true, + HealthChecks: make(map[string]HealthCheck), + }, + }, + Ingress: Ingress{ + Name: "cafe-ingress-master", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/mergeable-ingress-type": "master", + }, + }, + } +) + +var testUpstream = Upstream{ + Name: "test", + UpstreamZoneSize: "256k", + UpstreamServers: []UpstreamServer{ + { + Address: "127.0.0.1:8181", + MaxFails: 0, + MaxConns: 0, + FailTimeout: "1s", + SlowStart: "5s", + }, + }, +} + +var ( + headers = map[string]string{"Test-Header": "test-header-value"} + healthCheck = HealthCheck{ + UpstreamName: "test", + Fails: 1, + Interval: 1, + Passes: 1, + Headers: headers, + } +) diff --git a/internal/configs/version1/templates_test.go b/internal/configs/version1/templates_test.go deleted file mode 100644 index 137be17578..0000000000 --- a/internal/configs/version1/templates_test.go +++ /dev/null @@ -1,479 +0,0 @@ -package version1 - -import ( - "bytes" - "strings" - "testing" - "text/template" -) - -func makeTemplateNGINXPlus(t *testing.T) *template.Template { - t.Helper() - tmpl, err := template.New(nginxPlusIngressTmpl).Funcs(helperFunctions).ParseFiles(nginxPlusIngressTmpl) - if err != nil { - t.Fatal(err) - } - return tmpl -} - -func makeTemplateNGINX(t *testing.T) *template.Template { - t.Helper() - tmpl, err := template.New(nginxIngressTmpl).Funcs(helperFunctions).ParseFiles(nginxIngressTmpl) - if err != nil { - t.Fatal(err) - } - return tmpl -} - -func TestIngressForNGINXPlus(t *testing.T) { - t.Parallel() - tmpl := makeTemplateNGINXPlus(t) - buf := &bytes.Buffer{} - err := tmpl.Execute(buf, ingressCfg) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } -} - -func TestIngressForNGINX(t *testing.T) { - t.Parallel() - tmpl := makeTemplateNGINX(t) - buf := &bytes.Buffer{} - - err := tmpl.Execute(buf, ingressCfg) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } -} - -func TestExecuteTemplate_ForIngressForNGINXPlusWithRegExAnnotationCaseSensitive(t *testing.T) { - t.Parallel() - tmpl := makeTemplateNGINXPlus(t) - buf := &bytes.Buffer{} - - err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationCaseSensitive) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } - - wantLocation := "~ \"^/tea/[A-Z0-9]{3}\"" - if !strings.Contains(buf.String(), wantLocation) { - t.Errorf("want %q in generated config", wantLocation) - } -} - -func TestExecuteTemplate_ForIngressForNGINXPlusWithRegExAnnotationCaseInsensitive(t *testing.T) { - t.Parallel() - tmpl := makeTemplateNGINXPlus(t) - buf := &bytes.Buffer{} - - err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationCaseInsensitive) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } - - wantLocation := "~* \"^/tea/[A-Z0-9]{3}\"" - if !strings.Contains(buf.String(), wantLocation) { - t.Errorf("want %q in generated config", wantLocation) - } -} - -func TestExecuteTemplate_ForIngressForNGINXPlusWithRegExAnnotationExactMatch(t *testing.T) { - t.Parallel() - tmpl := makeTemplateNGINXPlus(t) - buf := &bytes.Buffer{} - - err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationExactMatch) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } - - wantLocation := "= \"/tea\"" - if !strings.Contains(buf.String(), wantLocation) { - t.Errorf("want %q in generated config", wantLocation) - } -} - -func TestExecuteTemplate_ForIngressForNGINXPlusWithRegExAnnotationEmpty(t *testing.T) { - t.Parallel() - tmpl := makeTemplateNGINXPlus(t) - buf := &bytes.Buffer{} - - err := tmpl.Execute(buf, ingressCfgWithRegExAnnotationEmptyString) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } - - wantLocation := "/tea" - if !strings.Contains(buf.String(), wantLocation) { - t.Errorf("want %q in generated config", wantLocation) - } -} - -func TestMainForNGINXPlus(t *testing.T) { - t.Parallel() - tmpl, err := template.New(nginxPlusMainTmpl).ParseFiles(nginxPlusMainTmpl) - if err != nil { - t.Fatalf("Failed to parse template file: %v", err) - } - buf := &bytes.Buffer{} - - err = tmpl.Execute(buf, mainCfg) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } -} - -func TestMainForNGINX(t *testing.T) { - t.Parallel() - tmpl, err := template.New(nginxMainTmpl).ParseFiles(nginxMainTmpl) - if err != nil { - t.Fatalf("Failed to parse template file: %v", err) - } - buf := &bytes.Buffer{} - - err = tmpl.Execute(buf, mainCfg) - t.Log(buf.String()) - if err != nil { - t.Fatalf("Failed to write template %v", err) - } -} - -var ( - // Ingress Config example without added annotations - ingressCfg = IngressNginxConfig{ - Servers: []Server{ - { - Name: "test.example.com", - ServerTokens: "off", - StatusZone: "test.example.com", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - RedirectLocationName: "@login_url-default-cafe-ingress", - }, - SSL: true, - SSLCertificate: "secret.pem", - SSLCertificateKey: "secret.pem", - SSLPorts: []int{443}, - SSLRedirect: true, - Locations: []Location{ - { - Path: "/tea", - Upstream: testUpstream, - ProxyConnectTimeout: "10s", - ProxyReadTimeout: "10s", - ProxySendTimeout: "10s", - ClientMaxBodySize: "2m", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/location-key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - }, - MinionIngress: &Ingress{ - Name: "tea-minion", - Namespace: "default", - }, - }, - }, - HealthChecks: map[string]HealthCheck{"test": healthCheck}, - JWTRedirectLocations: []JWTRedirectLocation{ - { - Name: "@login_url-default-cafe-ingress", - LoginURL: "https://test.example.com/login", - }, - }, - }, - }, - Upstreams: []Upstream{testUpstream}, - Keepalive: "16", - Ingress: Ingress{ - Name: "cafe-ingress", - Namespace: "default", - }, - } - - // Ingress Config example with path-regex annotation value "case_sensitive" - ingressCfgWithRegExAnnotationCaseSensitive = IngressNginxConfig{ - Servers: []Server{ - { - Name: "test.example.com", - ServerTokens: "off", - StatusZone: "test.example.com", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - RedirectLocationName: "@login_url-default-cafe-ingress", - }, - SSL: true, - SSLCertificate: "secret.pem", - SSLCertificateKey: "secret.pem", - SSLPorts: []int{443}, - SSLRedirect: true, - Locations: []Location{ - { - Path: "/tea/[A-Z0-9]{3}", - Upstream: testUpstream, - ProxyConnectTimeout: "10s", - ProxyReadTimeout: "10s", - ProxySendTimeout: "10s", - ClientMaxBodySize: "2m", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/location-key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - }, - MinionIngress: &Ingress{ - Name: "tea-minion", - Namespace: "default", - }, - }, - }, - HealthChecks: map[string]HealthCheck{"test": healthCheck}, - JWTRedirectLocations: []JWTRedirectLocation{ - { - Name: "@login_url-default-cafe-ingress", - LoginURL: "https://test.example.com/login", - }, - }, - }, - }, - Upstreams: []Upstream{testUpstream}, - Keepalive: "16", - Ingress: Ingress{ - Name: "cafe-ingress", - Namespace: "default", - Annotations: map[string]string{"nginx.org/path-regex": "case_sensitive"}, - }, - } - - // Ingress Config example with path-regex annotation value "case_insensitive" - ingressCfgWithRegExAnnotationCaseInsensitive = IngressNginxConfig{ - Servers: []Server{ - { - Name: "test.example.com", - ServerTokens: "off", - StatusZone: "test.example.com", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - RedirectLocationName: "@login_url-default-cafe-ingress", - }, - SSL: true, - SSLCertificate: "secret.pem", - SSLCertificateKey: "secret.pem", - SSLPorts: []int{443}, - SSLRedirect: true, - Locations: []Location{ - { - Path: "/tea/[A-Z0-9]{3}", - Upstream: testUpstream, - ProxyConnectTimeout: "10s", - ProxyReadTimeout: "10s", - ProxySendTimeout: "10s", - ClientMaxBodySize: "2m", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/location-key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - }, - MinionIngress: &Ingress{ - Name: "tea-minion", - Namespace: "default", - }, - }, - }, - HealthChecks: map[string]HealthCheck{"test": healthCheck}, - JWTRedirectLocations: []JWTRedirectLocation{ - { - Name: "@login_url-default-cafe-ingress", - LoginURL: "https://test.example.com/login", - }, - }, - }, - }, - Upstreams: []Upstream{testUpstream}, - Keepalive: "16", - Ingress: Ingress{ - Name: "cafe-ingress", - Namespace: "default", - Annotations: map[string]string{"nginx.org/path-regex": "case_insensitive"}, - }, - } - - // Ingress Config example with path-regex annotation value "exact" - ingressCfgWithRegExAnnotationExactMatch = IngressNginxConfig{ - Servers: []Server{ - { - Name: "test.example.com", - ServerTokens: "off", - StatusZone: "test.example.com", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - RedirectLocationName: "@login_url-default-cafe-ingress", - }, - SSL: true, - SSLCertificate: "secret.pem", - SSLCertificateKey: "secret.pem", - SSLPorts: []int{443}, - SSLRedirect: true, - Locations: []Location{ - { - Path: "/tea", - Upstream: testUpstream, - ProxyConnectTimeout: "10s", - ProxyReadTimeout: "10s", - ProxySendTimeout: "10s", - ClientMaxBodySize: "2m", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/location-key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - }, - MinionIngress: &Ingress{ - Name: "tea-minion", - Namespace: "default", - }, - }, - }, - HealthChecks: map[string]HealthCheck{"test": healthCheck}, - JWTRedirectLocations: []JWTRedirectLocation{ - { - Name: "@login_url-default-cafe-ingress", - LoginURL: "https://test.example.com/login", - }, - }, - }, - }, - Upstreams: []Upstream{testUpstream}, - Keepalive: "16", - Ingress: Ingress{ - Name: "cafe-ingress", - Namespace: "default", - Annotations: map[string]string{"nginx.org/path-regex": "exact"}, - }, - } - - // Ingress Config example with path-regex annotation value of an empty string - ingressCfgWithRegExAnnotationEmptyString = IngressNginxConfig{ - Servers: []Server{ - { - Name: "test.example.com", - ServerTokens: "off", - StatusZone: "test.example.com", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - RedirectLocationName: "@login_url-default-cafe-ingress", - }, - SSL: true, - SSLCertificate: "secret.pem", - SSLCertificateKey: "secret.pem", - SSLPorts: []int{443}, - SSLRedirect: true, - Locations: []Location{ - { - Path: "/tea", - Upstream: testUpstream, - ProxyConnectTimeout: "10s", - ProxyReadTimeout: "10s", - ProxySendTimeout: "10s", - ClientMaxBodySize: "2m", - JWTAuth: &JWTAuth{ - Key: "/etc/nginx/secrets/location-key.jwk", - Realm: "closed site", - Token: "$cookie_auth_token", - }, - MinionIngress: &Ingress{ - Name: "tea-minion", - Namespace: "default", - }, - }, - }, - HealthChecks: map[string]HealthCheck{"test": healthCheck}, - JWTRedirectLocations: []JWTRedirectLocation{ - { - Name: "@login_url-default-cafe-ingress", - LoginURL: "https://test.example.com/login", - }, - }, - }, - }, - Upstreams: []Upstream{testUpstream}, - Keepalive: "16", - Ingress: Ingress{ - Name: "cafe-ingress", - Namespace: "default", - Annotations: map[string]string{"nginx.org/path-regex": ""}, - }, - } - - mainCfg = MainConfig{ - ServerNamesHashMaxSize: "512", - ServerTokens: "off", - WorkerProcesses: "auto", - WorkerCPUAffinity: "auto", - WorkerShutdownTimeout: "1m", - WorkerConnections: "1024", - WorkerRlimitNofile: "65536", - LogFormat: []string{"$remote_addr", "$remote_user"}, - LogFormatEscaping: "default", - StreamSnippets: []string{"# comment"}, - StreamLogFormat: []string{"$remote_addr", "$remote_user"}, - StreamLogFormatEscaping: "none", - ResolverAddresses: []string{"example.com", "127.0.0.1"}, - ResolverIPV6: false, - ResolverValid: "10s", - ResolverTimeout: "15s", - KeepaliveTimeout: "65s", - KeepaliveRequests: 100, - VariablesHashBucketSize: 256, - VariablesHashMaxSize: 1024, - TLSPassthrough: true, - } -) - -const ( - nginxIngressTmpl = "nginx.ingress.tmpl" - nginxMainTmpl = "nginx.tmpl" - nginxPlusIngressTmpl = "nginx-plus.ingress.tmpl" - nginxPlusMainTmpl = "nginx-plus.tmpl" -) - -var testUpstream = Upstream{ - Name: "test", - UpstreamZoneSize: "256k", - UpstreamServers: []UpstreamServer{ - { - Address: "127.0.0.1:8181", - MaxFails: 0, - MaxConns: 0, - FailTimeout: "1s", - SlowStart: "5s", - }, - }, -} - -var ( - headers = map[string]string{"Test-Header": "test-header-value"} - healthCheck = HealthCheck{ - UpstreamName: "test", - Fails: 1, - Interval: 1, - Passes: 1, - Headers: headers, - } -)