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 7, 2023
1 parent dab0989 commit e4668d0
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 5 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,28 @@ prom-label-proxy \
`prom-label-proxy` will enforce the `tenant=~"prometheus|alertmanager"` label selector in all requests.
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
```
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
```
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
16 changes: 15 additions & 1 deletion 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 @@ -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()),
}
}
Expand Down
34 changes: 34 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,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) {
Expand All @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion injectproxy/silences.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
Expand Down
20 changes: 17 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,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)
}
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 threated as regexp.")

//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 e4668d0

Please sign in to comment.