InfluxDB: Fix template variable interpolation (#80971)

* use regex as templateSrv replace format

* use regex as templateSrv replace format for raw queries

* import path fix

* don't use regex formatter

* tag value escape

* tag value escape with wrappers

* polished interpolation logic

* update unit tests

* comments and more place to update

* unit test update

* fix escaping

* handle the string and array of string type separately

* update variable type
This commit is contained in:
ismail simsek
2024-02-01 00:30:21 +01:00
committed by GitHub
parent c9bdf69a46
commit 177fa1b947
6 changed files with 284 additions and 82 deletions

View File

@@ -13,8 +13,11 @@ import (
)
var (
regexpOperatorPattern = regexp.MustCompile(`^\/.*\/$`)
regexpMeasurementPattern = regexp.MustCompile(`^\/.*\/$`)
regexpOperatorPattern = regexp.MustCompile(`^\/.*\/$`)
regexpMeasurementPattern = regexp.MustCompile(`^\/.*\/$`)
regexMatcherSimple = regexp.MustCompile(`^/(.*)/$`)
regexMatcherWithStartEndPattern = regexp.MustCompile(`^/\^(.*)\$/$`)
mustEscapeCharsMatcher = regexp.MustCompile(`[\\^$*+?.()|[\]{}\/]`)
)
func (query *Query) Build(queryContext *backend.QueryDataRequest) (string, error) {
@@ -103,20 +106,20 @@ func (query *Query) renderTags() []string {
textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`))
}
return textValue, operator
return removeRegexWrappers(textValue, `'`), operator
}
// quote value unless regex or number
var textValue string
switch tag.Operator {
case "=~", "!~":
textValue = tag.Value
case "=~", "!~", "":
textValue = escape(tag.Value)
case "<", ">", ">=", "<=":
textValue = tag.Value
textValue = removeRegexWrappers(tag.Value, `'`)
case "Is", "Is Not":
textValue, tag.Operator = isOperatorTypeHandler(tag)
default:
textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`))
textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(removeRegexWrappers(tag.Value, ""), `\`, `\\`))
}
escapedKey := fmt.Sprintf(`"%s"`, tag.Key)
@@ -244,3 +247,56 @@ func epochMStoInfluxTime(tr *backend.TimeRange) (string, string) {
return fmt.Sprintf("%dms", from), fmt.Sprintf("%dms", to)
}
func removeRegexWrappers(wrappedValue string, wrapper string) string {
value := wrappedValue
// get the value only in between /^...$/
matches := regexMatcherWithStartEndPattern.FindStringSubmatch(wrappedValue)
if len(matches) > 1 {
// full match. the value is like /^value$/
value = wrapper + matches[1] + wrapper
}
return value
}
func escape(unescapedValue string) string {
pipe := `|`
beginning := `/^`
ending := `$/`
value := unescapedValue
substitute := `\$0`
fullMatch := false
// get the value only in between /^...$/
matches := regexMatcherWithStartEndPattern.FindStringSubmatch(unescapedValue)
if len(matches) > 1 {
// full match. the value is like /^value$/
value = matches[1]
fullMatch = true
}
if !fullMatch {
// get the value only in between /.../
matches = regexMatcherSimple.FindStringSubmatch(unescapedValue)
if len(matches) > 1 {
value = matches[1]
beginning = `/`
ending = `/`
}
}
// split them with pipe |
parts := strings.Split(value, pipe)
for i, v := range parts {
// escape each item
parts[i] = mustEscapeCharsMatcher.ReplaceAllString(v, substitute)
}
// stitch them to each other
escaped := make([]byte, 0, 64)
escaped = append(escaped, beginning...)
escaped = append(escaped, strings.Join(parts, pipe)...)
escaped = append(escaped, ending...)
return string(escaped)
}

View File

@@ -303,5 +303,53 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
require.Equal(t, query.renderMeasurement(), ` FROM "policy"./apa/`)
})
t.Run("can render regexp tags", func(t *testing.T) {
query := &Query{Tags: []*Tag{{Operator: "=~", Value: `/etc/hosts|/etc/hostname`, Key: "key"}}}
require.Equal(t, `"key" =~ /^\/etc\/hosts|\/etc\/hostname$/`, strings.Join(query.renderTags(), ""))
})
t.Run("can render regexp tags 2", func(t *testing.T) {
query := &Query{Tags: []*Tag{{Operator: "=~", Value: `/^/etc/hosts$/`, Key: "key"}}}
require.Equal(t, `"key" =~ /^\/etc\/hosts$/`, strings.Join(query.renderTags(), ""))
})
t.Run("can render regexp tags 3", func(t *testing.T) {
query := &Query{Tags: []*Tag{{Operator: "=~", Value: `/etc/hosts`, Key: "key"}}}
require.Equal(t, `"key" =~ /^\/etc\/hosts$/`, strings.Join(query.renderTags(), ""))
})
t.Run("can render regexp tags with dots in values", func(t *testing.T) {
query := &Query{Tags: []*Tag{{Operator: "=~", Value: `/etc/resolv.conf`, Key: "key"}}}
require.Equal(t, `"key" =~ /^\/etc\/resolv\.conf$/`, strings.Join(query.renderTags(), ""))
})
t.Run("can render single quoted tag value when regexed value has been sent", func(t *testing.T) {
query := &Query{Tags: []*Tag{{Operator: ">", Value: `/^12.2$/`, Key: "key"}}}
require.Equal(t, `"key" > '12.2'`, strings.Join(query.renderTags(), ""))
})
})
}
func TestRemoveRegexWrappers(t *testing.T) {
t.Run("remove regex wrappers", func(t *testing.T) {
wrappedText := `/^someValue$/`
expected := `'someValue'`
result := removeRegexWrappers(wrappedText, `'`)
require.Equal(t, expected, result)
})
t.Run("return same value if the value is not wrapped by regex wrappers", func(t *testing.T) {
wrappedText := `someValue`
expected := `someValue`
result := removeRegexWrappers(wrappedText, "")
require.Equal(t, expected, result)
})
}