Skip to content

Commit

Permalink
feat: enable multiple label values (#115)
Browse files Browse the repository at this point in the history
* Don't support multiple label values with the Silences API

* Do not overwrite existing filter with multi-values (Alertmanager)

When the original filter query parameter already includes the enforced
label, the proxy should preserve it as it indicates that the user wants
to filter down the list of silences/alerts.

* Do not overwrite existing filter when multi-values (Prometheus)

When prom-label-proxy is configured with multiple label values, it
should preserve the existing matchers because users actually want to
return results for a subset of the metrics. In practice when configured
with `namespace=~"bar|foo"`, the query `up{namespace="foo"}` is
translated into `up{namespace="foo",namespace=~"bar|foo"}` instead of
`{namespace=~"bar|foo"}`.

---------

Signed-off-by: Kirchen99 <latias_latios@126.com>
Signed-off-by: Kirchen99 <9981745+Kirchen99@users.noreply.github.com>
Signed-off-by: Simon Pasquier <spasquie@redhat.com>
Co-authored-by: Simon Pasquier <spasquie@redhat.com>
  • Loading branch information
Kirchen99 and simonpasquier authored Jun 15, 2023
1 parent c9b0c84 commit e4829ba
Show file tree
Hide file tree
Showing 13 changed files with 950 additions and 192 deletions.
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
prom-label-proxy

.idea
.envrc
prom-label-proxy
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ HTTP query parameter:
{"status":"success","data":{"resultType":"vector","result":[]}}%
```

You can provide multiple values for the label using several `tenant` HTTP query parameters:

```bash
~ curl http://127.0.0.1:8080/api/v1/query\?query="up"\&tenant\="something"\&tenant\="anything"
{"status":"success","data":{"resultType":"vector","result":[]}}%
```

It also works with POST requests:

```bash
Expand All @@ -105,6 +112,13 @@ prom-label-proxy \
{"status":"success","data":{"resultType":"vector","result":[]}}%
```
You can provide multiple values for the label using several HTTP headers:
```bash
➜ ~ curl -H 'X-Tenant=something' -H 'X-Tenant=anything' http://127.0.0.1:8080/api/v1/query\?query="up"
{"status":"success","data":{"resultType":"vector","result":[]}}%
```
A last option is to provide a static value for the label:
```
Expand All @@ -117,6 +131,19 @@ prom-label-proxy \
Now prom-label-proxy enforces the `tenant="prometheus"` label in all requests.
You can provide multiple static values for a label. For example:
```
prom-label-proxy \
-label tenant \
-label-value prometheus \
-label-value alertmanager \
-upstream http://demo.do.prometheus.io:9090 \
-insecure-listen-address 127.0.0.1:8080
```
`prom-label-proxy` will enforce the `tenant=~"prometheus|alertmanager"` label selector in all requests.
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 All @@ -138,7 +165,7 @@ and specifying the namespace label must be enforced to `b`, then the query will
```
http_requests_total{namespace="b"}
http_requests_total{namespace=~"b"}
```
This is enforced for any case, whether a label matcher is specified in the original query or not.
Expand Down Expand Up @@ -167,6 +194,8 @@ The proxy ensures the following:
* `POST` requests to the `/api/v2/silences` endpoint can only affect silences that match the label and the label matcher is enforced.
* `DELETE` requests to the `/api/v2/silence/` endpoint can only affect silences that match the label.
:rotating_light: `prom-label-proxy` doesn't support multiple label values for the Silences endpoints :rotating_light:
## Example use
The concrete setup being shipped in OpenShift starting with 4.0: the proxy is configured to work with the label-key: namespace. In order to ensure that this is secure is it paired with the [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) and its URL rewrite functionality, meaning first ServiceAccount token authentication is performed, and then the kube-rbac-proxy authorization to see whether the requesting entity is allowed to retrieve the metrics for the requested namespace. The RBAC role we chose to authorize against is the same as the Kubernetes Resource Metrics API, the reasoning being, if an entity can `kubectl top pod` in a namespace, it can see cAdvisor metrics (container_memory_rss, container_cpu_usage_seconds_total, etc.).
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/prometheus/alertmanager v0.25.0
github.com/prometheus/client_golang v1.15.1
github.com/prometheus/prometheus v0.44.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
)

require (
Expand Down Expand Up @@ -51,7 +52,6 @@ require (
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/goleak v1.2.1 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/sys v0.7.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions injectproxy/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package injectproxy

import "net/http"

// alerts proxies HTTP requests to the Alertmanager /api/v2/alerts endpoint.
func (r *routes) alerts(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case "GET":
Expand Down
40 changes: 34 additions & 6 deletions injectproxy/alerts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

func TestGetAlerts(t *testing.T) {
for _, tc := range []struct {
labelv string
labelv []string
filters []string
expCode int
expQueryValues []string
Expand All @@ -38,23 +38,48 @@ func TestGetAlerts(t *testing.T) {
},
{
// Check that other query parameters are not removed.
labelv: "default",
labelv: []string{"default"},
expCode: http.StatusOK,
expQueryValues: []string{"false"},
queryParam: "silenced",
url: "http://alertmanager.example.com/api/v2/alerts?silenced=false",
},
{
// Check that filter parameter is added when other query parameter are present
labelv: "default",
// Check that filter parameter is added when other query parameter are present.
labelv: []string{"default"},
expCode: http.StatusOK,
expQueryValues: []string{`namespace="default"`},
queryParam: "filter",
url: "http://alertmanager.example.com/api/v2/alerts?silenced=false",
},
{
// Check that the filter parameter is added when multiple label values are set.
labelv: []string{"default", "something"},
expCode: http.StatusOK,
expQueryValues: []string{`namespace=~"default|something"`},
queryParam: "filter",
url: "http://alertmanager.example.com/api/v2/alerts?silenced=false",
},
{
// Check that the original filter parameter is preserved when multiple label values are set.
labelv: []string{"default", "something"},
filters: []string{`namespace="default"`, `instance=~".+"`},
expCode: http.StatusOK,
expQueryValues: []string{`namespace=~"default|something"`, `namespace="default"`, `instance=~".+"`},
queryParam: "filter",
url: "http://alertmanager.example.com/api/v2/alerts?silenced=false",
},
{
// Check that label values are correctly escaped.
labelv: []string{"default", "some|thing"},
expCode: http.StatusOK,
expQueryValues: []string{`namespace=~"default|some\\|thing"`},
queryParam: "filter",
url: "http://alertmanager.example.com/api/v2/alerts?silenced=false",
},
{
// Check for filter parameter.
labelv: "default",
labelv: []string{"default"},
filters: []string{`job="prometheus"`, `instance=~".+"`},
expCode: http.StatusOK,
expQueryValues: []string{`job="prometheus"`, `instance=~".+"`, `namespace="default"`},
Expand All @@ -79,7 +104,10 @@ func TestGetAlerts(t *testing.T) {
for _, m := range tc.filters {
q.Add("filter", m)
}
q.Set(proxyLabel, tc.labelv)

for _, lv := range tc.labelv {
q.Add(proxyLabel, lv)
}
u.RawQuery = q.Encode()

w := httptest.NewRecorder()
Expand Down
13 changes: 10 additions & 3 deletions injectproxy/enforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ func (ms Enforcer) EnforceNode(node parser.Node) error {
// EnforceMatchers appends the configured label matcher if not present.
// If the label matcher that is to be injected is present (by labelname) but
// different (either by match type or value) the behavior depends on the
// errorOnReplace variable. If errorOnReplace is true an error is returned,
// otherwise the label matcher is silently replaced.
// errorOnReplace variable and the enforced matcher(s):
// * if errorOnReplace is true, an error is returned,
// * if errorOnReplace is false and the label matcher type is '=', the existing matcher is silently replaced.
// * otherwise the existing matcher is preserved.
func (ms Enforcer) EnforceMatchers(targets []*labels.Matcher) ([]*labels.Matcher, error) {
var res []*labels.Matcher

Expand All @@ -147,7 +149,12 @@ func (ms Enforcer) EnforceMatchers(targets []*labels.Matcher) ([]*labels.Matcher
if ms.errorOnReplace && matcher.String() != target.String() {
return res, newIllegalLabelMatcherError(matcher.String(), target.String())
}
continue

// Drop the existing matcher only if the enforced matcher is an
// equal matcher.
if matcher.Type == labels.MatchEqual {
continue
}
}

res = append(res, target)
Expand Down
Loading

0 comments on commit e4829ba

Please sign in to comment.