Files
grafana/pkg/services/ngalert/api/lotex_ruler.go
Kevin Yu fd6fd91115 Prometheus: Add support for cloud partners Prometheus data sources (#103482)
* wip

* Add prom flavor support for data source variables and export/import dashboards (#103321)

* add dashboard and data source var selection

* use match plugin id instead

* use updated matchpluginid

* formatting

* cleanup

* regex anchor

* update error msg

* Alerting: Clean up prometheus-flavored types and functions (#103703)

* clean up types and utility functions for dealing with
prometheus-flavored data sources

* Refactor alerting datasource types to use constants as source of truth

* Alerting: Clean up prometheus-flavored types and functions on the bac… (#103716)

Alerting: Clean up prometheus-flavored types and functions on the backend

* add matchPluginId tests

* Update matchPluginId func to bidirectional (#103746)

* update matchpluginid func to bidirectional

* lint

* formatting

* use actual isSupportedExternalRulesSourceType in test

* add tests in datasource_srv

* betterer

* remove type assertion

* remove unnecessary case

* use satisifies to not have to convert tuple to an array of string

* add prometheus_flavor test

---------

Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com>
Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
Co-authored-by: Alexander Akhmetov <me@alx.cx>
2025-04-10 12:49:11 -07:00

270 lines
6.7 KiB
Go

package api
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/web"
)
const (
Prometheus = "prometheus"
Cortex = "cortex"
Mimir = "mimir"
)
const (
mimirPrefix = "/config/v1/rules"
prometheusPrefix = "/rules"
lokiPrefix = "/api/prom/rules"
subtypeQuery = "subtype"
)
var subtypeToPrefix = map[string]string{
Prometheus: prometheusPrefix,
Cortex: prometheusPrefix,
Mimir: mimirPrefix,
}
// The requester is primarily used for testing purposes, allowing us to inject a different implementation of withReq.
type requester interface {
withReq(ctx *contextmodel.ReqContext, method string, u *url.URL, body io.Reader, extractor func(*response.NormalResponse) (any, error), headers map[string]string) response.Response
}
type LotexRuler struct {
log log.Logger
*AlertingProxy
requester requester
}
func NewLotexRuler(proxy *AlertingProxy, log log.Logger) *LotexRuler {
return &LotexRuler{
log: log,
AlertingProxy: proxy,
requester: proxy,
}
}
func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response {
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
if err != nil {
return ErrResp(500, err, "")
}
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
return r.requester.withReq(
ctx,
http.MethodDelete,
withPath(
*ctx.Req.URL,
fmt.Sprintf("%s/%s", legacyRulerPrefix, url.PathEscape(finalNamespace)),
),
nil,
messageExtractor,
nil,
)
}
func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response {
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
if err != nil {
return ErrResp(500, err, "")
}
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
finalGroup, err := getRulesGroupParam(ctx, group)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
return r.requester.withReq(
ctx,
http.MethodDelete,
withPath(
*ctx.Req.URL,
fmt.Sprintf(
"%s/%s/%s",
legacyRulerPrefix,
url.PathEscape(finalNamespace),
url.PathEscape(finalGroup),
),
),
nil,
messageExtractor,
nil,
)
}
func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response {
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
if err != nil {
return ErrResp(500, err, "")
}
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
return r.requester.withReq(
ctx,
http.MethodGet,
withPath(
*ctx.Req.URL,
fmt.Sprintf(
"%s/%s",
legacyRulerPrefix,
url.PathEscape(finalNamespace),
),
),
nil,
yamlExtractor(apimodels.NamespaceConfigResponse{}),
nil,
)
}
func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response {
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
if err != nil {
return ErrResp(500, err, "")
}
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
finalGroup, err := getRulesGroupParam(ctx, group)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
return r.requester.withReq(
ctx,
http.MethodGet,
withPath(
*ctx.Req.URL,
fmt.Sprintf(
"%s/%s/%s",
legacyRulerPrefix,
url.PathEscape(finalNamespace),
url.PathEscape(finalGroup),
),
),
nil,
yamlExtractor(&apimodels.GettableRuleGroupConfig{}),
nil,
)
}
func (r *LotexRuler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.Response {
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
if err != nil {
return ErrResp(500, err, "")
}
return r.requester.withReq(
ctx,
http.MethodGet,
withPath(
*ctx.Req.URL,
legacyRulerPrefix,
),
nil,
yamlExtractor(apimodels.NamespaceConfigResponse{}),
nil,
)
}
func (r *LotexRuler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, ns string) response.Response {
legacyRulerPrefix, err := r.validateAndGetPrefix(ctx)
if err != nil {
return ErrResp(500, err, "")
}
yml, err := yaml.Marshal(conf)
if err != nil {
return ErrResp(500, err, "Failed marshal rule group")
}
finalNamespace, err := getRulesNamespaceParam(ctx, ns)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
u := withPath(*ctx.Req.URL, fmt.Sprintf("%s/%s", legacyRulerPrefix, url.PathEscape(finalNamespace)))
return r.requester.withReq(ctx, http.MethodPost, u, bytes.NewBuffer(yml), jsonExtractor(nil), nil)
}
func (r *LotexRuler) validateAndGetPrefix(ctx *contextmodel.ReqContext) (string, error) {
datasourceUID := web.Params(ctx.Req)[":DatasourceUID"]
if datasourceUID == "" {
return "", fmt.Errorf("datasource UID is invalid")
}
ds, err := r.DataProxy.DataSourceCache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipDSCache)
if err != nil {
return "", err
}
// Validate URL
if ds.URL == "" {
return "", fmt.Errorf("URL for this data source is empty")
}
var prefix string
switch {
case isPrometheusCompatible(ds.Type):
prefix = prometheusPrefix
case ds.Type == datasources.DS_LOKI:
prefix = lokiPrefix
default:
return "", unexpectedDatasourceTypeError(ds.Type, "loki, prometheus, amazon prometheus, azure prometheus")
}
// If the datasource is Loki, there's nothing else for us to do - it doesn't have subtypes.
if ds.Type == datasources.DS_LOKI {
return prefix, nil
}
// A Prometheus datasource, can have many subtypes: Cortex, Mimir and vanilla Prometheus.
// Based on these subtypes, we want to use a different proxying path.
subtype := ctx.Query(subtypeQuery)
subTypePrefix, ok := subtypeToPrefix[subtype]
if !ok {
r.log.Debug(
"Unable to determine prometheus datasource subtype, using default prefix",
"datasource", ds.UID, "datasourceType", ds.Type, "subtype", subtype, "prefix", prefix)
return prefix, nil
}
r.log.Debug("Determined prometheus datasource subtype",
"datasource", ds.UID, "datasourceType", ds.Type, "subtype", subtype)
return subTypePrefix, nil
}
func withPath(u url.URL, newPath string) *url.URL {
u.Path, _ = url.PathUnescape(newPath)
u.RawPath = newPath
return &u
}