-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tenant-based filtering for Rules API response
Signed-off-by: Saswata Mukherjee <saswataminsta@yahoo.com>
- Loading branch information
1 parent
1852b0f
commit afdfbde
Showing
3 changed files
with
287 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package v1 | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/observatorium/api/authentication" | ||
"github.com/pkg/errors" | ||
"github.com/prometheus/prometheus/pkg/labels" | ||
) | ||
|
||
type apiResponse struct { | ||
Status string `json:"status"` | ||
Data json.RawMessage `json:"data,omitempty"` | ||
ErrorType string `json:"errorType,omitempty"` | ||
Error string `json:"error,omitempty"` | ||
Warnings []string `json:"warnings,omitempty"` | ||
} | ||
|
||
// getAPIResponse decodes /api/v1/rules response. | ||
// Adapted from https://github.com/prometheus-community/prom-label-proxy/blob/952266db4e0b8ab66b690501e532eaef33300596/injectproxy/rules.go#L37. | ||
func getAPIResponse(resp *http.Response) (*apiResponse, error) { | ||
defer resp.Body.Close() | ||
reader := resp.Body | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return nil, errors.Errorf("unexpected status code: %d", resp.StatusCode) | ||
} | ||
|
||
var apir apiResponse | ||
if err := json.NewDecoder(reader).Decode(&apir); err != nil { | ||
return nil, errors.Wrap(err, "JSON decoding") | ||
} | ||
|
||
if apir.Status != "success" { | ||
return nil, errors.Errorf("unexpected response status: %q", apir.Status) | ||
} | ||
|
||
return &apir, nil | ||
} | ||
|
||
type rulesData struct { | ||
RuleGroups []*ruleGroup `json:"groups"` | ||
} | ||
|
||
type ruleGroup struct { | ||
Name string `json:"name"` | ||
File string `json:"file"` | ||
Rules []rule `json:"rules"` | ||
Interval float64 `json:"interval"` | ||
EvaluationTime float64 `json:"evaluationTime,omitempty"` | ||
LastEvaluation *time.Time `json:"lastEvaluation,omitempty"` | ||
Limit int32 `json:"limit,omitempty"` | ||
// Thanos Querier specific field. | ||
PartialResponseStrategy string `json:"partialResponseStrategy"` | ||
} | ||
|
||
type rule struct { | ||
*alertingRule | ||
*recordingRule | ||
} | ||
|
||
func (r *rule) Labels() labels.Labels { | ||
if r.alertingRule != nil { | ||
return r.alertingRule.Labels | ||
} | ||
|
||
return r.recordingRule.Labels | ||
} | ||
|
||
// MarshalJSON implements the json.Marshaler interface for rule. | ||
func (r *rule) MarshalJSON() ([]byte, error) { | ||
if r.alertingRule != nil { | ||
return json.Marshal(r.alertingRule) | ||
} | ||
|
||
return json.Marshal(r.recordingRule) | ||
} | ||
|
||
// UnmarshalJSON implements the json.Unmarshaler interface for rule. | ||
func (r *rule) UnmarshalJSON(b []byte) error { | ||
var ruleType struct { | ||
Type string `json:"type"` | ||
} | ||
|
||
if err := json.Unmarshal(b, &ruleType); err != nil { | ||
return err | ||
} | ||
|
||
switch ruleType.Type { | ||
case "alerting": | ||
var alertingr alertingRule | ||
if err := json.Unmarshal(b, &alertingr); err != nil { | ||
return err | ||
} | ||
|
||
r.alertingRule = &alertingr | ||
case "recording": | ||
var recordingr recordingRule | ||
if err := json.Unmarshal(b, &recordingr); err != nil { | ||
return err | ||
} | ||
|
||
r.recordingRule = &recordingr | ||
default: | ||
return errors.Errorf("failed to unmarshal rule: unknown type %q", ruleType.Type) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type alertingRule struct { | ||
Name string `json:"name"` | ||
Query string `json:"query"` | ||
Duration float64 `json:"duration"` | ||
Labels labels.Labels `json:"labels"` | ||
Annotations labels.Labels `json:"annotations"` | ||
Alerts []*alert `json:"alerts"` | ||
Health string `json:"health"` | ||
LastError string `json:"lastError,omitempty"` | ||
// Type of an alertingRule is always "alerting". | ||
Type string `json:"type"` | ||
} | ||
|
||
type recordingRule struct { | ||
Name string `json:"name"` | ||
Query string `json:"query"` | ||
Labels labels.Labels `json:"labels,omitempty"` | ||
Health string `json:"health"` | ||
LastError string `json:"lastError,omitempty"` | ||
EvaluationTime float64 `json:"evaluationTime,omitempty"` | ||
LastEvaluation *time.Time `json:"lastEvaluation,omitempty"` | ||
// Type of a recordingRule is always "recording". | ||
Type string `json:"type"` | ||
} | ||
|
||
type alert struct { | ||
Labels labels.Labels `json:"labels"` | ||
Annotations labels.Labels `json:"annotations"` | ||
State string `json:"state"` | ||
ActiveAt *time.Time `json:"activeAt,omitempty"` | ||
Value string `json:"value"` | ||
} | ||
|
||
// ModifyRulesAPIResponse modifies the /api/v1/rules API response by passing the enforced tenant label and id to f(). | ||
// Adapted from https://github.com/prometheus-community/prom-label-proxy/blob/952266db4e0b8ab66b690501e532eaef33300596/injectproxy/rules.go#L166. | ||
func ModifyRulesAPIResponse(label string, f func(string, string, *apiResponse) (interface{}, error)) func(*http.Response) error { | ||
return func(resp *http.Response) error { | ||
if resp.StatusCode != http.StatusOK { | ||
// Pass non-200 responses as-is. | ||
return nil | ||
} | ||
|
||
id, ok := authentication.GetTenantID(resp.Request.Context()) | ||
if !ok { | ||
return errors.New("error finding tenant ID") | ||
} | ||
|
||
apir, err := getAPIResponse(resp) | ||
if err != nil { | ||
return errors.Wrap(err, "reading API response") | ||
} | ||
|
||
r, err := f(label, id, apir) | ||
if err != nil { | ||
return errors.Wrap(err, "error extracting rules") | ||
} | ||
|
||
b, err := json.Marshal(r) | ||
if err != nil { | ||
return errors.Wrap(err, "can't replace data") | ||
} | ||
apir.Data = json.RawMessage(b) | ||
|
||
var buf bytes.Buffer | ||
if err = json.NewEncoder(&buf).Encode(apir); err != nil { | ||
return errors.Wrap(err, "can't encode API response") | ||
} | ||
resp.Body = ioutil.NopCloser(&buf) | ||
resp.Header["Content-Length"] = []string{fmt.Sprint(buf.Len())} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
// Filters rules based on given label and value. | ||
// Adapted from https://github.com/prometheus-community/prom-label-proxy/blob/952266db4e0b8ab66b690501e532eaef33300596/injectproxy/rules.go#L200. | ||
func FilterRules(label string, value string, resp *apiResponse) (interface{}, error) { | ||
var rgs rulesData | ||
if err := json.Unmarshal(resp.Data, &rgs); err != nil { | ||
return nil, errors.Wrap(err, "can't decode rules data") | ||
} | ||
|
||
filtered := []*ruleGroup{} | ||
|
||
for _, rg := range rgs.RuleGroups { | ||
var rules []rule | ||
|
||
for _, rule := range rg.Rules { | ||
for _, lbl := range rule.Labels() { | ||
if lbl.Name == label && lbl.Value == value { | ||
rules = append(rules, rule) | ||
break | ||
} | ||
} | ||
} | ||
|
||
if len(rules) > 0 { | ||
rg.Rules = rules | ||
filtered = append(filtered, rg) | ||
} | ||
} | ||
|
||
return &rulesData{RuleGroups: filtered}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters