Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support regex for non-query endpoints #226

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ NOTE: When the `/api/v1/labels` and `/api/v1/label/<name>/values` endpoints were

### Rules endpoint

The proxy requests the `/api/v1/rules` Prometheus endpoint, discards the rules that don't contain an exact match of the label and returns the modified response to the client.
The proxy requests the `/api/v1/rules` Prometheus endpoint, discards the rules that don't contain an exact match of the label(s) and returns the modified response to the client.

### Alerts endpoint

The proxy requests the `/api/v1/alerts` Prometheus endpoint, discards the rules that don't contain an exact match of the label and returns the modified response to the client.
The proxy requests the `/api/v1/alerts` Prometheus endpoint, discards the rules that don't contain an exact match of the label(s) and returns the modified response to the client.

### Silences endpoint

Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ require (
github.com/prometheus/alertmanager v0.27.0
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/prometheus v0.52.1
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
gotest.tools/v3 v3.5.1
)

Expand Down
52 changes: 48 additions & 4 deletions injectproxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o
"/api/v1/rules": modifyAPIResponse(r.filterRules),
"/api/v1/alerts": modifyAPIResponse(r.filterAlerts),
}
//FIXME: when ModifyResponse returns an error, the default ErrorHandler is
//called which returns 502 Bad Gateway. It'd be more appropriate to treat
//the error and return 400 in case of bad input for instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

proxy.ModifyResponse = r.ModifyResponse
return r, nil
}
Expand Down Expand Up @@ -577,21 +580,62 @@ func enforceQueryValues(e *PromQLEnforcer, v url.Values) (values string, noQuery
return v.Encode(), true, nil
}

func (r *routes) newLabelMatcher(vals ...string) (*labels.Matcher, error) {
if r.regexMatch {
if len(vals) != 1 {
return nil, errors.New("only one label value allowed with regex match")
}

re := vals[0]
compiledRegex, err := regexp.Compile(re)
if err != nil {
return nil, fmt.Errorf("invalid regex: %w", err)
}

if compiledRegex.MatchString("") {
return nil, errors.New("regex should not match empty string")
}

m, err := labels.NewMatcher(labels.MatchRegexp, r.label, re)
if err != nil {
return nil, err
}

return m, nil
}

if len(vals) == 1 {
return &labels.Matcher{
Name: r.label,
Type: labels.MatchEqual,
Value: vals[0],
}, nil
}

m, err := labels.NewMatcher(labels.MatchRegexp, r.label, labelValuesToRegexpString(vals))
if err != nil {
return nil, err
}

return m, nil
}

// matcher modifies all the match[] HTTP parameters to match on the tenant label.
// If none was provided, a tenant label matcher matcher is injected.
// This works for non-query Prometheus API endpoints like /api/v1/series,
// /api/v1/label/<name>/values, /api/v1/labels and /federate which support
// multiple matchers.
// See e.g https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metadata
func (r *routes) matcher(w http.ResponseWriter, req *http.Request) {
matcher := &labels.Matcher{
Name: r.label,
Type: labels.MatchRegexp,
Value: labelValuesToRegexpString(MustLabelValues(req.Context())),
matcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...)
if err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
return
}

q := req.URL.Query()
if err := injectMatcher(q, matcher); err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
return
}

Expand Down
78 changes: 61 additions & 17 deletions injectproxy/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func checkParameterAbsent(param string, next http.Handler) http.Handler {
prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError)
return
}

if len(kvs[param]) != 0 {
prometheusAPIError(w, fmt.Sprintf("unexpected parameter %q", param), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -264,6 +265,7 @@ func TestMatch(t *testing.T) {
for _, tc := range []struct {
labelv []string
matches []string
opts []Option

expCode int
expMatch []string
Expand All @@ -277,15 +279,15 @@ func TestMatch(t *testing.T) {
// No "match" parameter.
labelv: []string{"default"},
expCode: http.StatusOK,
expMatch: []string{`{namespace=~"default"}`},
expMatch: []string{`{namespace="default"}`},
expBody: okResponse,
},
{
// Single "match" parameters.
labelv: []string{"default"},
matches: []string{`{job="prometheus",__name__=~"job:.*"}`},
expCode: http.StatusOK,
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default"}`},
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
expBody: okResponse,
},
{
Expand All @@ -309,15 +311,15 @@ func TestMatch(t *testing.T) {
labelv: []string{"default"},
matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
expCode: http.StatusOK,
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace=~"default"}`},
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`},
expBody: okResponse,
},
{
// Many "match" parameters.
labelv: []string{"default"},
matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`},
expCode: http.StatusOK,
expMatch: []string{`{job="prometheus",namespace=~"default"}`, `{__name__=~"job:.*",namespace=~"default"}`},
expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`},
expBody: okResponse,
},
{
Expand All @@ -336,6 +338,42 @@ func TestMatch(t *testing.T) {
},
expBody: okResponse,
},
{
// Many "match" parameters with a single regex value.
labelv: []string{".+-monitoring"},
matches: []string{
`{job="prometheus"}`,
`{__name__=~"job:.*"}`,
`{namespace="something"}`,
},
opts: []Option{WithRegexMatch()},

expCode: http.StatusOK,
expMatch: []string{
`{job="prometheus",namespace=~".+-monitoring"}`,
`{__name__=~"job:.*",namespace=~".+-monitoring"}`,
`{namespace="something",namespace=~".+-monitoring"}`,
},
expBody: okResponse,
},
{
// A single "match" parameter with multiple regex values.
labelv: []string{"default", "something"},
matches: []string{
`{job="prometheus"}`,
},
opts: []Option{WithRegexMatch()},
expCode: http.StatusBadRequest,
},
{
// A single "match" parameter with a regex value matching the empty string.
labelv: []string{".*"},
matches: []string{
`{job="prometheus"}`,
},
opts: []Option{WithRegexMatch()},
expCode: http.StatusBadRequest,
},
} {
for _, u := range []string{
"http://prometheus.example.com/federate",
Expand All @@ -351,7 +389,12 @@ func TestMatch(t *testing.T) {
)
defer m.Close()

r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithEnabledLabelsAPI())
r, err := NewRoutes(
m.url,
proxyLabel,
HTTPFormEnforcer{ParameterName: proxyLabel},
append([]Option{WithEnabledLabelsAPI()}, tc.opts...)...,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down Expand Up @@ -382,6 +425,7 @@ func TestMatch(t *testing.T) {
t.Logf("%s", string(body))
t.FailNow()
}

if resp.StatusCode != http.StatusOK {
return
}
Expand Down Expand Up @@ -411,15 +455,15 @@ func TestMatchWithPost(t *testing.T) {
// No "match" parameter.
labelv: []string{"default"},
expCode: http.StatusOK,
expMatch: []string{`{namespace=~"default"}`},
expMatch: []string{`{namespace="default"}`},
expBody: okResponse,
},
{
// Single "match" parameters.
labelv: []string{"default"},
matches: []string{`{job="prometheus",__name__=~"job:.*"}`},
expCode: http.StatusOK,
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default"}`},
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
expBody: okResponse,
},
{
Expand All @@ -443,15 +487,15 @@ func TestMatchWithPost(t *testing.T) {
labelv: []string{"default"},
matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
expCode: http.StatusOK,
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace=~"default"}`},
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`},
expBody: okResponse,
},
{
// Many "match" parameters.
labelv: []string{"default"},
matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`},
expCode: http.StatusOK,
expMatch: []string{`{job="prometheus",namespace=~"default"}`, `{__name__=~"job:.*",namespace=~"default"}`},
expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`},
expBody: okResponse,
},
{
Expand Down Expand Up @@ -546,14 +590,14 @@ func TestSeries(t *testing.T) {
{
name: `No "match[]" parameter returns 200 with empty body`,
labelv: []string{"default"},
expMatch: []string{`{namespace=~"default"}`},
expMatch: []string{`{namespace="default"}`},
expResponse: okResponse,
expCode: http.StatusOK,
},
{
name: `No "match[]" parameter returns 200 with empty body for POSTs`,
labelv: []string{"default"},
expMatch: []string{`{namespace=~"default"}`},
expMatch: []string{`{namespace="default"}`},
expResponse: okResponse,
expCode: http.StatusOK,
},
Expand All @@ -562,7 +606,7 @@ func TestSeries(t *testing.T) {
labelv: []string{"default"},
promQuery: "up",
expCode: http.StatusOK,
expMatch: []string{`{__name__="up",namespace=~"default"}`},
expMatch: []string{`{__name__="up",namespace="default"}`},
expResponse: okResponse,
},
{
Expand All @@ -586,7 +630,7 @@ func TestSeries(t *testing.T) {
labelv: []string{"default"},
promQuery: `up{instance="localhost:9090"}`,
expCode: http.StatusOK,
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace=~"default"}`},
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`},
expResponse: okResponse,
},
{
Expand Down Expand Up @@ -679,15 +723,15 @@ func TestSeriesWithPost(t *testing.T) {
name: `No "match[]" parameter returns 200 with empty body`,
labelv: []string{"default"},
method: http.MethodPost,
expMatch: []string{`{namespace=~"default"}`},
expMatch: []string{`{namespace="default"}`},
expResponse: okResponse,
expCode: http.StatusOK,
},
{
name: `No "match[]" parameter returns 200 with empty body for POSTs`,
method: http.MethodPost,
labelv: []string{"default"},
expMatch: []string{`{namespace=~"default"}`},
expMatch: []string{`{namespace="default"}`},
expResponse: okResponse,
expCode: http.StatusOK,
},
Expand All @@ -697,7 +741,7 @@ func TestSeriesWithPost(t *testing.T) {
promQueryBody: "up",
method: http.MethodPost,
expCode: http.StatusOK,
expMatch: []string{`{__name__="up",namespace=~"default"}`},
expMatch: []string{`{__name__="up",namespace="default"}`},
expResponse: okResponse,
},
{
Expand All @@ -724,7 +768,7 @@ func TestSeriesWithPost(t *testing.T) {
promQueryBody: `up{instance="localhost:9090"}`,
method: http.MethodPost,
expCode: http.StatusOK,
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace=~"default"}`},
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`},
expResponse: okResponse,
},
{
Expand Down
16 changes: 13 additions & 3 deletions injectproxy/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"time"

"github.com/prometheus/prometheus/model/labels"
"golang.org/x/exp/slices"
)

type apiResponse struct {
Expand Down Expand Up @@ -210,14 +209,20 @@ func (r *routes) filterRules(lvalues []string, resp *apiResponse) (interface{},
return nil, fmt.Errorf("can't decode rules data: %w", err)
}

m, err := r.newLabelMatcher(lvalues...)
if err != nil {
return nil, err
}

filtered := []*ruleGroup{}
for _, rg := range rgs.RuleGroups {
var rules []rule
for _, rule := range rg.Rules {
if lval := rule.Labels().Get(r.label); lval != "" && slices.Contains(lvalues, lval) {
if lval := rule.Labels().Get(r.label); lval != "" && m.Matches(lval) {
rules = append(rules, rule)
}
}

if len(rules) > 0 {
rg.Rules = rules
filtered = append(filtered, rg)
Expand All @@ -233,9 +238,14 @@ func (r *routes) filterAlerts(lvalues []string, resp *apiResponse) (interface{},
return nil, fmt.Errorf("can't decode alerts data: %w", err)
}

m, err := r.newLabelMatcher(lvalues...)
if err != nil {
return nil, err
}

filtered := []*alert{}
for _, alert := range data.Alerts {
if lval := alert.Labels.Get(r.label); lval != "" && slices.Contains(lvalues, lval) {
if lval := alert.Labels.Get(r.label); lval != "" && m.Matches(lval) {
filtered = append(filtered, alert)
}
}
Expand Down
Loading