diff --git a/agent/consul/prepared_query/template.go b/agent/consul/prepared_query/template.go index f758de2ce6..5b7b48421f 100644 --- a/agent/consul/prepared_query/template.go +++ b/agent/consul/prepared_query/template.go @@ -29,6 +29,10 @@ type CompiledTemplate struct { // re is the compiled regexp, if they supplied one (this can be nil). re *regexp.Regexp + + // removeEmptyTags will cause the service tags to be stripped of any + // empty strings after interpolation. + removeEmptyTags bool } // Compile validates a prepared query template and returns an opaque compiled @@ -41,7 +45,8 @@ func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) { // Start compile. ct := &CompiledTemplate{ - trees: make(map[string]ast.Node), + trees: make(map[string]ast.Node), + removeEmptyTags: query.Template.RemoveEmptyTags, } // Make a copy of the query to use as the basis for rendering later. @@ -181,5 +186,15 @@ func (ct *CompiledTemplate) Render(name string) (*structs.PreparedQuery, error) return nil, err } + if ct.removeEmptyTags { + tags := make([]string, 0, len(query.Service.Tags)) + for _, tag := range query.Service.Tags { + if tag != "" { + tags = append(tags, tag) + } + } + query.Service.Tags = tags + } + return query, nil } diff --git a/agent/consul/prepared_query/template_test.go b/agent/consul/prepared_query/template_test.go index 32d54117bb..5fe7fb7a75 100644 --- a/agent/consul/prepared_query/template_test.go +++ b/agent/consul/prepared_query/template_test.go @@ -15,8 +15,9 @@ var ( bigBench = &structs.PreparedQuery{ Name: "hello", Template: structs.QueryTemplateOptions{ - Type: structs.QueryTemplateTypeNamePrefixMatch, - Regexp: "^hello-(.*)-(.*)$", + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^hello-(.*)-(.*)$", + RemoveEmptyTags: true, }, Service: structs.ServiceQuery{ Service: "${name.full}", @@ -296,4 +297,84 @@ func TestTemplate_Render(t *testing.T) { t.Fatalf("bad: %#v", actual) } } + + // Try all the variables and functions, removing empty tags. + query = &structs.PreparedQuery{ + Name: "hello-", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^(.*?)-(.*?)-(.*)$", + RemoveEmptyTags: true, + }, + Service: structs.ServiceQuery{ + Service: "${name.prefix} xxx ${name.full} xxx ${name.suffix}", + Tags: []string{ + "${match(-1)}", + "${match(0)}", + "${match(1)}", + "${match(2)}", + "${match(3)}", + "${match(4)}", + "${40 + 2}", + }, + }, + } + ct, err = Compile(query) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Run a case that matches the regexp, removing empty tags. + { + actual, err := ct.Render("hello-foo-bar-none") + if err != nil { + t.Fatalf("err: %v", err) + } + expected := &structs.PreparedQuery{ + Name: "hello-", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^(.*?)-(.*?)-(.*)$", + RemoveEmptyTags: true, + }, + Service: structs.ServiceQuery{ + Service: "hello- xxx hello-foo-bar-none xxx foo-bar-none", + Tags: []string{ + "hello-foo-bar-none", + "hello", + "foo", + "bar-none", + "42", + }, + }, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\n%#v\nexpected:\n%#v\n", actual, expected) + } + } + + // Run a case that doesn't match the regexp, removing empty tags. + { + actual, err := ct.Render("hello-nope") + if err != nil { + t.Fatalf("err: %v", err) + } + expected := &structs.PreparedQuery{ + Name: "hello-", + Template: structs.QueryTemplateOptions{ + Type: structs.QueryTemplateTypeNamePrefixMatch, + Regexp: "^(.*?)-(.*?)-(.*)$", + RemoveEmptyTags: true, + }, + Service: structs.ServiceQuery{ + Service: "hello- xxx hello-nope xxx nope", + Tags: []string{ + "42", + }, + }, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } + } } diff --git a/agent/consul/structs/prepared_query.go b/agent/consul/structs/prepared_query.go index af535f010b..7967b85397 100644 --- a/agent/consul/structs/prepared_query.go +++ b/agent/consul/structs/prepared_query.go @@ -68,6 +68,9 @@ type QueryTemplateOptions struct { // used to extract parts of the name and choose a service name, set // tags, etc. Regexp string + + // RemoveEmptyTags, if true, removes empty tags from matched tag list + RemoveEmptyTags bool } // PreparedQuery defines a complete prepared query, and is the structure we diff --git a/website/source/api/query.html.md b/website/source/api/query.html.md index 3e476f130c..5f419a8925 100644 --- a/website/source/api/query.html.md +++ b/website/source/api/query.html.md @@ -32,7 +32,8 @@ Here is an example prepared query template: { "Template": { "Type": "name_prefix_match", - "Regexp": "^geo-db-(.*?)-([^\\-]+?)$" + "Regexp": "^geo-db-(.*?)-([^\\-]+?)$", + "RemoveEmptyTags": false } } ``` @@ -55,6 +56,12 @@ static query. It has two fields: [RE2](https://github.com/google/re2/wiki/Syntax) reference for syntax of this regular expression. +- `RemoveEmptyTags` is optional, and if set to true, will cause the `Tags` list + inside the `Service` structure to be stripped of any empty strings. This defaults + to false, meaning that empty strings will remain in the list. This is useful + when interpolating into tags in a way where the tag is optional, and where + searching for an empty tag would yield no results from the query. + All other fields of the query have the same meanings as for a static query, except that several interpolation variables are available to dynamically populate the query before it is executed. All of the string fields inside the