From 9a52e028d5b03c409fb02d06a9aa72b11bc32675 Mon Sep 17 00:00:00 2001 From: Ian Yong Date: Tue, 7 Nov 2023 22:24:05 +0800 Subject: [PATCH] Add string comparison functions to Go template executors (#4560) --- .../ingress-resources/custom-annotations.md | 11 +- internal/configs/version1/template_helper.go | 5 + .../configs/version1/template_helper_test.go | 184 +++++++++++++++++ internal/configs/version2/template_helper.go | 15 +- .../configs/version2/template_helper_test.go | 191 ++++++++++++++++++ 5 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 internal/configs/version2/template_helper_test.go diff --git a/docs/content/configuration/ingress-resources/custom-annotations.md b/docs/content/configuration/ingress-resources/custom-annotations.md index b44da0b6d8..bd927d3b08 100644 --- a/docs/content/configuration/ingress-resources/custom-annotations.md +++ b/docs/content/configuration/ingress-resources/custom-annotations.md @@ -107,10 +107,15 @@ If you'd like to use custom annotations with Mergeable Ingress resources, please Helper functions can be used in the Ingress template to parse the values of custom annotations. {{% table %}} -|Function | Input Arguments | Return Arguments | Description | +| Function | Input Arguments | Return Arguments | Description | | ---| ---| ---| --- | -|``split`` | ``s, sep string`` | ``[]string`` | Splits the string ``s`` into a slice of strings separated by the ``sep``. | -|``trim`` | ``s string`` | ``string`` | Trims the trailing and leading whitespace from the string ``s``. | +| ``split`` | ``s, sep string`` | ``[]string`` | Splits the string ``s`` into a slice of strings separated by the ``sep``. | +| ``trim`` | ``s string`` | ``string`` | Trims the trailing and leading whitespace from the string ``s``. | +| ``contains`` | ``s, substr string`` | ``bool`` | Tests whether the string ``substr`` is a substring of the string ``s``. | +| ``hasPrefix`` | ``s, prefix string`` | ``bool`` | Tests whether the string ``prefix`` is a prefix of the string ``s``. | +| ``hasSuffix`` | ``s, suffix string`` | ``bool`` | Tests whether the string ``suffix`` is a suffix of the string ``s``. | +| ``toLower`` | ``s string`` | ``bool`` | Converts all letters in the string ``s`` to their lower case. | +| ``toUpper`` | ``s string`` | ``bool`` | Converts all letters in the string ``s`` to their upper case. | {{% /table %}} Consider the following custom annotation `custom.nginx.org/allowed-ips`, which expects a comma-separated list of IP addresses: diff --git a/internal/configs/version1/template_helper.go b/internal/configs/version1/template_helper.go index e94ef8c971..4b18903bc5 100644 --- a/internal/configs/version1/template_helper.go +++ b/internal/configs/version1/template_helper.go @@ -65,5 +65,10 @@ func makePathWithRegex(path, regexType string) string { var helperFunctions = template.FuncMap{ "split": split, "trim": trim, + "contains": strings.Contains, + "hasPrefix": strings.HasPrefix, + "hasSuffix": strings.HasSuffix, + "toLower": strings.ToLower, + "toUpper": strings.ToUpper, "makeLocationPath": makeLocationPath, } diff --git a/internal/configs/version1/template_helper_test.go b/internal/configs/version1/template_helper_test.go index 7e0ee01226..10a833c377 100644 --- a/internal/configs/version1/template_helper_test.go +++ b/internal/configs/version1/template_helper_test.go @@ -274,6 +274,145 @@ func TestTrimWhiteSpaceFromInputString(t *testing.T) { } } +func TestContainsSubstring(t *testing.T) { + t.Parallel() + + tmpl := newContainsTemplate(t) + testCases := []struct { + InputString string + Substring string + expected string + }{ + {InputString: "foo", Substring: "foo", expected: "true"}, + {InputString: "foobar", Substring: "foo", expected: "true"}, + {InputString: "foo", Substring: "", expected: "true"}, + {InputString: "foo", Substring: "bar", expected: "false"}, + {InputString: "foo", Substring: "foobar", expected: "false"}, + {InputString: "", Substring: "foo", expected: "false"}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestHasPrefix(t *testing.T) { + t.Parallel() + + tmpl := newHasPrefixTemplate(t) + testCases := []struct { + InputString string + Prefix string + expected string + }{ + {InputString: "foo", Prefix: "foo", expected: "true"}, + {InputString: "foo", Prefix: "f", expected: "true"}, + {InputString: "foo", Prefix: "", expected: "true"}, + {InputString: "foo", Prefix: "oo", expected: "false"}, + {InputString: "foo", Prefix: "bar", expected: "false"}, + {InputString: "foo", Prefix: "foobar", expected: "false"}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestHasSuffix(t *testing.T) { + t.Parallel() + + tmpl := newHasSuffixTemplate(t) + testCases := []struct { + InputString string + Suffix string + expected string + }{ + {InputString: "bar", Suffix: "bar", expected: "true"}, + {InputString: "bar", Suffix: "r", expected: "true"}, + {InputString: "bar", Suffix: "", expected: "true"}, + {InputString: "bar", Suffix: "ba", expected: "false"}, + {InputString: "bar", Suffix: "foo", expected: "false"}, + {InputString: "bar", Suffix: "foobar", expected: "false"}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestToLowerInputString(t *testing.T) { + t.Parallel() + + tmpl := newToLowerTemplate(t) + testCases := []struct { + InputString string + expected string + }{ + {InputString: "foobar", expected: "foobar"}, + {InputString: "FOOBAR", expected: "foobar"}, + {InputString: "fOoBaR", expected: "foobar"}, + {InputString: "", expected: ""}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestToUpperInputString(t *testing.T) { + t.Parallel() + + tmpl := newToUpperTemplate(t) + testCases := []struct { + InputString string + expected string + }{ + {InputString: "foobar", expected: "FOOBAR"}, + {InputString: "FOOBAR", expected: "FOOBAR"}, + {InputString: "fOoBaR", expected: "FOOBAR"}, + {InputString: "", expected: ""}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + func newSplitTemplate(t *testing.T) *template.Template { t.Helper() tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{range $n := split . ","}}{{$n}} {{end}}`) @@ -291,3 +430,48 @@ func newTrimTemplate(t *testing.T) *template.Template { } return tmpl } + +func newContainsTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{contains .InputString .Substring}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newHasPrefixTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{hasPrefix .InputString .Prefix}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newHasSuffixTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{hasSuffix .InputString .Suffix}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newToLowerTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{toLower .InputString}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newToUpperTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{toUpper .InputString}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} diff --git a/internal/configs/version2/template_helper.go b/internal/configs/version2/template_helper.go index 86767fa6ae..22289e3e6a 100644 --- a/internal/configs/version2/template_helper.go +++ b/internal/configs/version2/template_helper.go @@ -20,17 +20,12 @@ func hasCIKey(key string, d map[string]string) bool { return ok } -// toLower takes a string and make it lowercase. -// -// Example: -// -// {{ if .SameSite}} samesite={{.SameSite | toLower }}{{ end }} -func toLower(s string) string { - return strings.ToLower(s) -} - var helperFunctions = template.FuncMap{ "headerListToCIMap": headerListToCIMap, "hasCIKey": hasCIKey, - "toLower": toLower, + "contains": strings.Contains, + "hasPrefix": strings.HasPrefix, + "hasSuffix": strings.HasSuffix, + "toLower": strings.ToLower, + "toUpper": strings.ToUpper, } diff --git a/internal/configs/version2/template_helper_test.go b/internal/configs/version2/template_helper_test.go new file mode 100644 index 0000000000..dfba45a8b3 --- /dev/null +++ b/internal/configs/version2/template_helper_test.go @@ -0,0 +1,191 @@ +package version2 + +import ( + "bytes" + "testing" + "text/template" +) + +func TestContainsSubstring(t *testing.T) { + t.Parallel() + + tmpl := newContainsTemplate(t) + testCases := []struct { + InputString string + Substring string + expected string + }{ + {InputString: "foo", Substring: "foo", expected: "true"}, + {InputString: "foobar", Substring: "foo", expected: "true"}, + {InputString: "foo", Substring: "", expected: "true"}, + {InputString: "foo", Substring: "bar", expected: "false"}, + {InputString: "foo", Substring: "foobar", expected: "false"}, + {InputString: "", Substring: "foo", expected: "false"}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestHasPrefix(t *testing.T) { + t.Parallel() + + tmpl := newHasPrefixTemplate(t) + testCases := []struct { + InputString string + Prefix string + expected string + }{ + {InputString: "foo", Prefix: "foo", expected: "true"}, + {InputString: "foo", Prefix: "f", expected: "true"}, + {InputString: "foo", Prefix: "", expected: "true"}, + {InputString: "foo", Prefix: "oo", expected: "false"}, + {InputString: "foo", Prefix: "bar", expected: "false"}, + {InputString: "foo", Prefix: "foobar", expected: "false"}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestHasSuffix(t *testing.T) { + t.Parallel() + + tmpl := newHasSuffixTemplate(t) + testCases := []struct { + InputString string + Suffix string + expected string + }{ + {InputString: "bar", Suffix: "bar", expected: "true"}, + {InputString: "bar", Suffix: "r", expected: "true"}, + {InputString: "bar", Suffix: "", expected: "true"}, + {InputString: "bar", Suffix: "ba", expected: "false"}, + {InputString: "bar", Suffix: "foo", expected: "false"}, + {InputString: "bar", Suffix: "foobar", expected: "false"}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestToLowerInputString(t *testing.T) { + t.Parallel() + + tmpl := newToLowerTemplate(t) + testCases := []struct { + InputString string + expected string + }{ + {InputString: "foobar", expected: "foobar"}, + {InputString: "FOOBAR", expected: "foobar"}, + {InputString: "fOoBaR", expected: "foobar"}, + {InputString: "", expected: ""}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func TestToUpperInputString(t *testing.T) { + t.Parallel() + + tmpl := newToUpperTemplate(t) + testCases := []struct { + InputString string + expected string + }{ + {InputString: "foobar", expected: "FOOBAR"}, + {InputString: "FOOBAR", expected: "FOOBAR"}, + {InputString: "fOoBaR", expected: "FOOBAR"}, + {InputString: "", expected: ""}, + } + + for _, tc := range testCases { + var buf bytes.Buffer + err := tmpl.Execute(&buf, tc) + if err != nil { + t.Fatalf("Failed to execute the template %v", err) + } + if buf.String() != tc.expected { + t.Errorf("Template generated wrong config, got %v but expected %v.", buf.String(), tc.expected) + } + } +} + +func newContainsTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{contains .InputString .Substring}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newHasPrefixTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{hasPrefix .InputString .Prefix}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newHasSuffixTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{hasSuffix .InputString .Suffix}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newToLowerTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{toLower .InputString}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +} + +func newToUpperTemplate(t *testing.T) *template.Template { + t.Helper() + tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{toUpper .InputString}}`) + if err != nil { + t.Fatalf("Failed to parse template: %v", err) + } + return tmpl +}