Skip to content

Commit

Permalink
Accept label value as regexp
Browse files Browse the repository at this point in the history
Fixes: #82
Signed-off-by: Mathieu Parent <[email protected]>
  • Loading branch information
sathieu committed Dec 11, 2023
1 parent b4be3b0 commit 9d4dfd7
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 8 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,30 @@ prom-label-proxy \
`prom-label-proxy` will enforce the `tenant=~"prometheus|alertmanager"` label selector in all requests.
You can match label value using a regular expression with the `-regex-match` option. For example:
```
prom-label-proxy \
-header-name X-Namespace \
-label namespace \
-upstream http://demo.do.prometheus.io:9090 \
-insecure-listen-address 127.0.0.1:8080 \
-regex-match
```
> :warning: The above feature is experimental. Be careful when using this option, it can expose sensitive metrics by accident.
To error out when the query already contains a label matcher that differs from the one the proxy would inject, you can use the `-error-on-replace` option. For example:
```
prom-label-proxy \
-header-name X-Namespace \
-label namespace \
-upstream http://demo.do.prometheus.io:9090 \
-insecure-listen-address 127.0.0.1:8080 \
-error-on-replace
```
Once again for clarity: **this project only enforces a particular label in the respective calls to Prometheus, it in itself does not authenticate or
authorize the requesting entity in any way, this has to be built around this project.**
Expand Down
26 changes: 24 additions & 2 deletions injectproxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ type routes struct {
mux http.Handler
modifiers map[string]func(*http.Response) error
errorOnReplace bool
regexMatch bool
}

type options struct {
enableLabelAPIs bool
passthroughPaths []string
errorOnReplace bool
registerer prometheus.Registerer
regexMatch bool
}

type Option interface {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -438,16 +448,28 @@ func (r *routes) query(w http.ResponseWriter, req *http.Request) {
var matcher *labels.Matcher

if len(MustLabelValues(req.Context())) > 1 {
if r.regexMatch {
prometheusAPIError(w, "Only one label value allowed with regex match", http.StatusBadRequest)
}
matcher = &labels.Matcher{
Name: r.label,
Type: labels.MatchRegexp,
Value: labelValuesToRegexpString(MustLabelValues(req.Context())),
}
} else {
matcherType := labels.MatchEqual
matcherValue := MustLabelValue(req.Context())
if r.regexMatch {
if _, err := regexp.Compile(matcherValue); err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
return
}
matcherType = labels.MatchRegexp
}
matcher = &labels.Matcher{
Name: r.label,
Type: labels.MatchEqual,
Value: MustLabelValue(req.Context()),
Type: matcherType,
Value: matcherValue,
}
}

Expand Down
52 changes: 52 additions & 0 deletions injectproxy/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ func TestQuery(t *testing.T) {
expPromQueryBody string
expResponse []byte
errorOnReplace bool
regexMatch bool
}{
{
name: `No "namespace" parameter returns an error`,
Expand Down Expand Up @@ -1121,6 +1122,54 @@ 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,
},
{
name: `HTTP header with invalid 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.StatusBadRequest,
// {"error":"error parsing regexp: missing closing ): `tenant1-(.*`","errorType":"prom-label-proxy","status":"error"}
},
{
name: `Multiple regexp HTTP headers is invalid`,
headers: http.Header{"namespace": []string{"tenant1", "tenant2"}},
headerName: "namespace",
regexMatch: true,
promQuery: `up{instance="localhost:9090"} + foo{namespace="tenant1-.*"}`,
expCode: http.StatusBadRequest,
// {"error":"Only one label value allowed with regex match","errorType":"prom-label-proxy","status":"error"}
},
} {
for _, endpoint := range []string{"query", "query_range", "query_exemplars"} {
t.Run(endpoint+"/"+strings.ReplaceAll(tc.name, " ", "_"), func(t *testing.T) {
Expand All @@ -1140,6 +1189,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 {
Expand Down
2 changes: 1 addition & 1 deletion injectproxy/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func TestRules(t *testing.T) {
labelv: []string{"not_present_gzip_requested"},
upstream: gzipHandler(validRules()),
reqHeaders: map[string][]string{
"Accept-Encoding": []string{"gzip"},
"Accept-Encoding": {"gzip"},
},

expCode: http.StatusOK,
Expand Down
14 changes: 12 additions & 2 deletions injectproxy/silences.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"net/http"
"path"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -73,10 +74,19 @@ func (r *routes) enforceFilterParameter(w http.ResponseWriter, req *http.Request
Value: labelValuesToRegexpString(MustLabelValues(req.Context())),
}
} else {
matcherType := labels.MatchEqual
matcherValue := MustLabelValue(req.Context())
if r.regexMatch {
if _, err := regexp.Compile(matcherValue); err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
return
}
matcherType = labels.MatchRegexp
}
proxyLabelMatch = labels.Matcher{
Type: labels.MatchEqual,
Type: matcherType,
Name: r.label,
Value: MustLabelValue(req.Context()),
Value: matcherValue,
}
}

Expand Down
35 changes: 32 additions & 3 deletions injectproxy/silences_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,11 +75,39 @@ 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,
},
{
// Invalid regex match
labelv: []string{"tenant1-(.*"},
regexMatch: true,
filters: []string{`namespace=~"foo|default"`, `job="prometheus"`},
expCode: http.StatusBadRequest,
},
{
// Multiple regex is invalid
labelv: []string{"tenant1", "tenant2"},
regexMatch: true,
filters: []string{`namespace=~"foo|default"`, `job="prometheus"`},
expCode: http.StatusUnprocessableEntity,
// Multiple label matchers not supported
},
} {
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)
}
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(&regexMatch, "regex-match", false, "When specified, the tenant name is treated as a regular expression.")

//nolint: errcheck // Parse() will exit on error.
flagset.Parse(os.Args[1:])
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 9d4dfd7

Please sign in to comment.