diff --git a/injectproxy/routes.go b/injectproxy/routes.go index 266d3226..03b00503 100644 --- a/injectproxy/routes.go +++ b/injectproxy/routes.go @@ -46,6 +46,7 @@ type routes struct { mux http.Handler modifiers map[string]func(*http.Response) error errorOnReplace bool + regexMatch bool } type options struct { @@ -53,6 +54,7 @@ type options struct { passthroughPaths []string errorOnReplace bool registerer prometheus.Registerer + regexMatch bool } type Option interface { @@ -96,6 +98,13 @@ func WithErrorOnReplace() Option { }) } +// WithRegexMatch causes the proxy to handle tenant name as regexp +func WithRegexMatch() Option { + return optionFunc(func(o *options) { + o.regexMatch = true + }) +} + // mux abstracts away the behavior we expect from the http.ServeMux type in this package. type mux interface { http.Handler @@ -279,6 +288,7 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o label: label, el: extractLabeler, errorOnReplace: opt.errorOnReplace, + regexMatch: opt.regexMatch, } mux := newStrictMux(newInstrumentedMux(http.NewServeMux(), opt.registerer)) @@ -444,9 +454,13 @@ func (r *routes) query(w http.ResponseWriter, req *http.Request) { Value: labelValuesToRegexpString(MustLabelValues(req.Context())), } } else { + matcherType := labels.MatchEqual + if r.regexMatch { + matcherType = labels.MatchRegexp + } matcher = &labels.Matcher{ Name: r.label, - Type: labels.MatchEqual, + Type: matcherType, Value: MustLabelValue(req.Context()), } } diff --git a/injectproxy/routes_test.go b/injectproxy/routes_test.go index 5b114044..4006db51 100644 --- a/injectproxy/routes_test.go +++ b/injectproxy/routes_test.go @@ -813,6 +813,7 @@ func TestQuery(t *testing.T) { expPromQueryBody string expResponse []byte errorOnReplace bool + regexMatch bool }{ { name: `No "namespace" parameter returns an error`, @@ -1121,6 +1122,36 @@ func TestQuery(t *testing.T) { expPromQuery: `up{instance="localhost:9090",namespace="default"} + foo{namespace="default"}`, expResponse: okResponse, }, + { + name: `HTTP header as regexp`, + headers: http.Header{"namespace": []string{"tenant1-.*"}}, + headerName: "namespace", + regexMatch: true, + promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, + expCode: http.StatusOK, + expPromQuery: `up{instance="localhost:9090",namespace=~"tenant1-.*"} + foo{namespace="other",namespace=~"tenant1-.*"}`, + expResponse: okResponse, + }, + { + name: `query param as regexp`, + queryParam: "namespace", + labelv: []string{"tenant1-.*"}, + regexMatch: true, + promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, + expCode: http.StatusOK, + expPromQuery: `up{instance="localhost:9090",namespace=~"tenant1-.*"} + foo{namespace="other",namespace=~"tenant1-.*"}`, + expResponse: okResponse, + }, + { + name: `HTTP header as regexp with same regexp in query`, + headers: http.Header{"namespace": []string{"tenant1-.*"}}, + headerName: "namespace", + regexMatch: true, + promQuery: `up{instance="localhost:9090"} + foo{namespace="tenant1-.*"}`, + expCode: http.StatusOK, + expPromQuery: `up{instance="localhost:9090",namespace=~"tenant1-.*"} + foo{namespace="tenant1-.*",namespace=~"tenant1-.*"}`, + expResponse: okResponse, + }, } { for _, endpoint := range []string{"query", "query_range", "query_exemplars"} { t.Run(endpoint+"/"+strings.ReplaceAll(tc.name, " ", "_"), func(t *testing.T) { @@ -1140,6 +1171,9 @@ func TestQuery(t *testing.T) { if tc.errorOnReplace { opts = append(opts, WithErrorOnReplace()) } + if tc.regexMatch { + opts = append(opts, WithRegexMatch()) + } var labelEnforcer ExtractLabeler if len(tc.staticLabelVal) > 0 { diff --git a/injectproxy/silences.go b/injectproxy/silences.go index 7a09137d..dec53a8d 100644 --- a/injectproxy/silences.go +++ b/injectproxy/silences.go @@ -73,8 +73,12 @@ func (r *routes) enforceFilterParameter(w http.ResponseWriter, req *http.Request Value: labelValuesToRegexpString(MustLabelValues(req.Context())), } } else { + matcherType := labels.MatchEqual + if r.regexMatch { + matcherType = labels.MatchRegexp + } proxyLabelMatch = labels.Matcher{ - Type: labels.MatchEqual, + Type: matcherType, Name: r.label, Value: MustLabelValue(req.Context()), } diff --git a/injectproxy/silences_test.go b/injectproxy/silences_test.go index 22d48dd8..0e4b4473 100644 --- a/injectproxy/silences_test.go +++ b/injectproxy/silences_test.go @@ -29,8 +29,9 @@ import ( func TestListSilences(t *testing.T) { for _, tc := range []struct { - labelv []string - filters []string + labelv []string + filters []string + regexMatch bool expCode int expFilters []string @@ -74,11 +75,24 @@ func TestListSilences(t *testing.T) { labelv: []string{"default", "something"}, expCode: http.StatusUnprocessableEntity, }, + { + // Regex match + labelv: []string{"tenant1-.*"}, + regexMatch: true, + filters: []string{`namespace=~"foo|default"`, `job="prometheus"`}, + expCode: http.StatusOK, + expFilters: []string{`namespace=~"foo|default"`, `namespace=~"tenant1-.*"`, `job="prometheus"`}, + expBody: okResponse, + }, } { t.Run(strings.Join(tc.filters, "&"), func(t *testing.T) { m := newMockUpstream(checkQueryHandler("", "filter", tc.expFilters...)) defer m.Close() - r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}) + var opts []Option + if tc.regexMatch { + opts = append(opts, WithRegexMatch()) + } + r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, opts...) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/main.go b/main.go index 9c3520ff..93f5f8d2 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,7 @@ func main() { enableLabelAPIs bool unsafePassthroughPaths string // Comma-delimited string. errorOnReplace bool + regexMatch bool ) flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError) @@ -81,6 +82,7 @@ func main() { "This option is checked after Prometheus APIs, you cannot override enforced API endpoints to be not enforced with this option. Use carefully as it can easily cause a data leak if the provided path is an important "+ "API (like /api/v1/configuration) which isn't enforced by prom-label-proxy. NOTE: \"all\" matching paths like \"/\" or \"\" and regex are not allowed.") flagset.BoolVar(&errorOnReplace, "error-on-replace", false, "When specified, the proxy will return HTTP status code 400 if the query already contains a label matcher that differs from the one the proxy would inject.") + flagset.BoolVar(®exMatch, "regex-match", false, "When specified, the tenant name is threated as regexp.") //nolint: errcheck // Parse() will exit on error. flagset.Parse(os.Args[1:]) @@ -125,6 +127,9 @@ func main() { if errorOnReplace { opts = append(opts, injectproxy.WithErrorOnReplace()) } + if regexMatch { + opts = append(opts, injectproxy.WithRegexMatch()) + } var extractLabeler injectproxy.ExtractLabeler switch {