Alerting: API paths for cortextool to import Loki rules (#101409)

Alerting: Legacy rules paths for cortextool
This commit is contained in:
Alexander Akhmetov
2025-02-27 17:20:49 +01:00
committed by GitHub
parent 843d876f16
commit ef86582dfc
12 changed files with 1049 additions and 255 deletions
+10 -4
View File
@@ -125,26 +125,32 @@ func (api *API) authorize(method, path string) web.Handler {
// convert/prometheus API paths
case http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}",
http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
)
case http.MethodGet + "/api/convert/prometheus/config/v1/rules":
case http.MethodGet + "/api/convert/prometheus/config/v1/rules",
http.MethodGet + "/api/convert/api/prom/rules":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
)
case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodPost + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleCreate),
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
)
case http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}",
http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAny(
ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
@@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 63)
require.Len(t, paths, 66)
ac := acmock.New()
api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()}
@@ -19,6 +19,12 @@ import (
)
type ConvertPrometheusApi interface {
RouteConvertPrometheusCortexDeleteNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexDeleteRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexGetNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexGetRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response
@@ -27,6 +33,36 @@ type ConvertPrometheusApi interface {
RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
return f.handleRouteConvertPrometheusCortexDeleteNamespace(ctx, namespaceTitleParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx, namespaceTitleParam, groupParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
return f.handleRouteConvertPrometheusCortexGetNamespace(ctx, namespaceTitleParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteConvertPrometheusCortexGetRuleGroup(ctx, namespaceTitleParam, groupParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusCortexGetRules(ctx)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
return f.handleRouteConvertPrometheusCortexPostRuleGroup(ctx, namespaceTitleParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@@ -60,6 +96,78 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *c
func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Delete(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}"),
metrics.Instrument(
http.MethodDelete,
"/api/convert/api/prom/rules/{NamespaceTitle}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteNamespace),
m,
),
)
group.Delete(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
metrics.Instrument(
http.MethodDelete,
"/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteRuleGroup),
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/prom/rules/{NamespaceTitle}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetNamespace),
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRuleGroup),
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/prom/rules"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/prom/rules"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/prom/rules",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRules),
m,
),
)
group.Post(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/convert/api/prom/rules/{NamespaceTitle}"),
metrics.Instrument(
http.MethodPost,
"/api/convert/api/prom/rules/{NamespaceTitle}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexPostRuleGroup),
m,
),
)
group.Delete(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
@@ -20,6 +20,7 @@ func NewConvertPrometheusApi(svc *ConvertPrometheusSrv) *ConvertPrometheusApiHan
}
}
// mimirtool
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteConvertPrometheusGetRules(ctx)
}
@@ -54,3 +55,28 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup(
return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup)
}
// cortextool
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusGetRules(ctx)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
return f.handleRouteConvertPrometheusDeleteNamespace(ctx, namespaceTitle)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitle, group)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
return f.handleRouteConvertPrometheusGetNamespace(ctx, namespaceTitle)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
return f.handleRouteConvertPrometheusGetRuleGroup(ctx, namespaceTitle, group)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle)
}
+2 -2
View File
@@ -4493,7 +4493,6 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -4529,7 +4528,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4932,6 +4931,7 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert",
"type": "object"
@@ -4,6 +4,7 @@ import (
"github.com/prometheus/common/model"
)
// Route for mimirtool
// swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules
//
// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.
@@ -16,6 +17,20 @@ import (
// 403: ForbiddenError
// 404: NotFound
// Route for cortextool
// swagger:route GET /convert/api/prom/rules convert_prometheus RouteConvertPrometheusCortexGetRules
//
// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.
//
// Produces:
// - application/yaml
//
// Responses:
// 200: PrometheusNamespace
// 403: ForbiddenError
// 404: NotFound
// Route for mimirtool
// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace
//
// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).
@@ -28,6 +43,20 @@ import (
// 403: ForbiddenError
// 404: NotFound
// Route for cortextool
// swagger:route GET /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexGetNamespace
//
// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).
//
// Produces:
// - application/yaml
//
// Responses:
// 200: PrometheusNamespace
// 403: ForbiddenError
// 404: NotFound
// Route for mimirtool
// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup
//
// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.
@@ -40,6 +69,20 @@ import (
// 403: ForbiddenError
// 404: NotFound
// Route for cortextool
// swagger:route GET /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexGetRuleGroup
//
// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.
//
// Produces:
// - application/yaml
//
// Responses:
// 200: PrometheusRuleGroup
// 403: ForbiddenError
// 404: NotFound
// Route for mimirtool
// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup
//
// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.
@@ -59,6 +102,27 @@ import (
// Extensions:
// x-raw-request: true
// Route for cortextool
// swagger:route POST /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexPostRuleGroup
//
// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.
// If the group already exists and was not imported from a Prometheus-compatible source initially,
// it will not be replaced and an error will be returned.
//
// Consumes:
// - application/yaml
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
//
// Extensions:
// x-raw-request: true
// Route for mimirtool
// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace
//
// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.
@@ -70,6 +134,19 @@ import (
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// Route for cortextool
// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexDeleteNamespace
//
// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// Route for mimirtool
// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup
//
// Deletes a specific rule group if it was imported from a Prometheus-compatible source.
@@ -81,7 +158,19 @@ import (
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// swagger:parameters RouteConvertPrometheusPostRuleGroup
// Route for cortextool
// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexDeleteRuleGroup
//
// Deletes a specific rule group if it was imported from a Prometheus-compatible source.
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup
type RouteConvertPrometheusPostRuleGroupParams struct {
// in: path
NamespaceTitle string
@@ -119,7 +208,7 @@ type PrometheusRule struct {
Record string `yaml:"record,omitempty"`
}
// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusGetRuleGroup
// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup
type RouteConvertPrometheusDeleteRuleGroupParams struct {
// in: path
NamespaceTitle string
@@ -127,7 +216,7 @@ type RouteConvertPrometheusDeleteRuleGroupParams struct {
Group string
}
// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusGetNamespace
// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusCortexDeleteNamespace RouteConvertPrometheusGetNamespace RouteConvertPrometheusCortexGetNamespace
type RouteConvertPrometheusDeleteNamespaceParams struct {
// in: path
NamespaceTitle string
+251 -1
View File
@@ -4493,6 +4493,7 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -4528,7 +4529,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4770,6 +4771,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup",
"type": "object"
@@ -5056,6 +5058,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence",
"type": "object"
@@ -6398,6 +6401,253 @@
]
}
},
"/convert/api/prom/rules": {
"get": {
"operationId": "RouteConvertPrometheusCortexGetRules",
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.",
"tags": [
"convert_prometheus"
]
}
},
"/convert/api/prom/rules/{NamespaceTitle}": {
"delete": {
"operationId": "RouteConvertPrometheusCortexDeleteNamespace",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.",
"tags": [
"convert_prometheus"
]
},
"get": {
"operationId": "RouteConvertPrometheusCortexGetNamespace",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
}
],
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).",
"tags": [
"convert_prometheus"
]
},
"post": {
"consumes": [
"application/yaml"
],
"description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.",
"operationId": "RouteConvertPrometheusCortexPostRuleGroup",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
},
{
"in": "header",
"name": "x-grafana-alerting-datasource-uid",
"type": "string"
},
{
"in": "header",
"name": "x-grafana-alerting-recording-rules-paused",
"type": "boolean"
},
{
"in": "header",
"name": "x-grafana-alerting-alert-rules-paused",
"type": "boolean"
},
{
"in": "body",
"name": "Body",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.",
"tags": [
"convert_prometheus"
],
"x-raw-request": "true"
}
},
"/convert/api/prom/rules/{NamespaceTitle}/{Group}": {
"delete": {
"operationId": "RouteConvertPrometheusCortexDeleteRuleGroup",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "Group",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.",
"tags": [
"convert_prometheus"
]
},
"get": {
"operationId": "RouteConvertPrometheusCortexGetRuleGroup",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "Group",
"required": true,
"type": "string"
}
],
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "PrometheusRuleGroup",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.",
"tags": [
"convert_prometheus"
]
}
},
"/convert/prometheus/config/v1/rules": {
"get": {
"operationId": "RouteConvertPrometheusGetRules",
+251 -1
View File
@@ -1102,6 +1102,253 @@
}
}
},
"/convert/api/prom/rules": {
"get": {
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.",
"operationId": "RouteConvertPrometheusCortexGetRules",
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
}
},
"/convert/api/prom/rules/{NamespaceTitle}": {
"get": {
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).",
"operationId": "RouteConvertPrometheusCortexGetNamespace",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
},
"post": {
"description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.",
"consumes": [
"application/yaml"
],
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.",
"operationId": "RouteConvertPrometheusCortexPostRuleGroup",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
},
{
"type": "string",
"name": "x-grafana-alerting-datasource-uid",
"in": "header"
},
{
"type": "boolean",
"name": "x-grafana-alerting-recording-rules-paused",
"in": "header"
},
{
"type": "boolean",
"name": "x-grafana-alerting-alert-rules-paused",
"in": "header"
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"x-raw-request": "true"
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.",
"operationId": "RouteConvertPrometheusCortexDeleteNamespace",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
},
"/convert/api/prom/rules/{NamespaceTitle}/{Group}": {
"get": {
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.",
"operationId": "RouteConvertPrometheusCortexGetRuleGroup",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "PrometheusRuleGroup",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.",
"operationId": "RouteConvertPrometheusCortexDeleteRuleGroup",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
},
"/convert/prometheus/config/v1/rules": {
"get": {
"produces": [
@@ -8434,8 +8681,9 @@
}
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"type": "object",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -8711,6 +8959,7 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"type": "object",
@@ -8997,6 +9246,7 @@
}
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"type": "object",
@@ -99,283 +99,315 @@ var (
)
func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
// Setup Grafana and its Database
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
apiClient.prometheusConversionUseLokiPaths = enableLokiPaths
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password")
namespace1 := "test-namespace-1"
namespace2 := "test-namespace-2"
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
t.Run("create rule groups and get them back", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// create a third group in a different namespace
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// And a non-provisioned rule in another namespace
namespace3UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace3UID, "folder")
createRule(t, apiClient, namespace3UID)
// Now get the first group
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name)
require.Equal(t, promGroup1, group1)
// Get namespace1
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1)
expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
}
require.Equal(t, expectedNs1, ns1)
// Get all namespaces
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
})
t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) {
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusForbidden, status, raw)
})
t.Run("delete one rule group", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name)
// Check that the promGroup2 and promGroup3 are still there
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
// Delete the second namespace
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2)
// Check that only the first namespace is left
namespaces = apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
}
require.Equal(t, expectedNamespaces, namespaces)
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password")
namespace1 := "test-namespace-1"
namespace2 := "test-namespace-2"
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
t.Run("create rule groups and get them back", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// create a third group in a different namespace
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// And a non-provisioned rule in another namespace
namespace3UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace3UID, "folder")
createRule(t, apiClient, namespace3UID)
// Now get the first group
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name)
require.Equal(t, promGroup1, group1)
// Get namespace1
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1)
expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
}
require.Equal(t, expectedNs1, ns1)
// Get all namespaces
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
})
t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) {
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusForbidden, status, raw)
})
t.Run("delete one rule group", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name)
// Check that the promGroup2 and promGroup3 are still there
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
// Delete the second namespace
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2)
// Check that only the first namespace is left
namespaces = apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
}
require.Equal(t, expectedNamespaces, namespaces)
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
// Setup Grafana and its Database
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
apiClient.prometheusConversionUseLokiPaths = enableLokiPaths
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
namespace1 := "test-namespace-1"
namespace1 := "test-namespace-1"
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) {
// Create a rule group using the provisioning API and then try to overwrite it
// using the Prometheus Conversion API. It should fail because the provenance
// we set for rules in these two APIs is different and we check that when updating.
provisionedRuleGroup := apimodels.AlertRuleGroup{
Title: promGroup1.Name,
Interval: 60,
FolderUID: namespace1,
Rules: []apimodels.ProvisionedAlertRule{
{
Title: "Rule1",
OrgID: 1,
RuleGroup: promGroup1.Name,
Condition: "A",
NoDataState: apimodels.Alerting,
ExecErrState: apimodels.AlertingErrState,
For: prommodel.Duration(time.Duration(60) * time.Second),
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) {
// Create a rule group using the provisioning API and then try to overwrite it
// using the Prometheus Conversion API. It should fail because the provenance
// we set for rules in these two APIs is different and we check that when updating.
provisionedRuleGroup := apimodels.AlertRuleGroup{
Title: promGroup1.Name,
Interval: 60,
FolderUID: namespace1,
Rules: []apimodels.ProvisionedAlertRule{
{
Title: "Rule1",
OrgID: 1,
RuleGroup: promGroup1.Name,
Condition: "A",
NoDataState: apimodels.Alerting,
ExecErrState: apimodels.AlertingErrState,
For: prommodel.Duration(time.Duration(60) * time.Second),
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
},
},
},
}
}
// Create the folder
apiClient.CreateFolder(t, namespace1, namespace1)
// Create rule in the root folder using another API
_, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup)
require.Equalf(t, http.StatusOK, status, response)
// Create the folder
apiClient.CreateFolder(t, namespace1, namespace1)
// Create rule in the root folder using another API
_, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup)
require.Equalf(t, http.StatusOK, status, response)
// Should fail to post the group
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusConflict, status, body)
// Should fail to post the group
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusConflict, status, body)
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
namespace1 := "test-namespace-1"
namespace1 := "test-namespace-1"
namespace1UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace1UID, namespace1)
namespace1UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace1UID, namespace1)
t.Run("when pausing header is set, rules should be paused", func(t *testing.T) {
tests := []struct {
name string
recordingPaused bool
alertPaused bool
}{
{
name: "do not pause rules",
recordingPaused: false,
alertPaused: false,
},
{
name: "pause recording rules",
recordingPaused: true,
alertPaused: false,
},
{
name: "pause alert rules",
recordingPaused: false,
alertPaused: true,
},
{
name: "pause both recording and alert rules",
recordingPaused: true,
alertPaused: true,
},
}
t.Run("when pausing header is set, rules should be paused", func(t *testing.T) {
tests := []struct {
name string
recordingPaused bool
alertPaused bool
}{
{
name: "do not pause rules",
recordingPaused: false,
alertPaused: false,
},
{
name: "pause recording rules",
recordingPaused: true,
alertPaused: false,
},
{
name: "pause alert rules",
recordingPaused: false,
alertPaused: true,
},
{
name: "pause both recording and alert rules",
recordingPaused: true,
alertPaused: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
headers := map[string]string{}
if tc.recordingPaused {
headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true"
}
if tc.alertPaused {
headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true"
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
headers := map[string]string{}
if tc.recordingPaused {
headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true"
}
if tc.alertPaused {
headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true"
}
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers)
gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name)
gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name)
require.Len(t, gr.Rules, 3)
require.Len(t, gr.Rules, 3)
pausedRecordingRules := 0
pausedAlertRules := 0
pausedRecordingRules := 0
pausedAlertRules := 0
for _, rule := range gr.Rules {
if rule.GrafanaManagedAlert.IsPaused {
if rule.GrafanaManagedAlert.Record != nil {
pausedRecordingRules++
} else {
pausedAlertRules++
for _, rule := range gr.Rules {
if rule.GrafanaManagedAlert.IsPaused {
if rule.GrafanaManagedAlert.Record != nil {
pausedRecordingRules++
} else {
pausedAlertRules++
}
}
}
}
if tc.recordingPaused {
require.Equal(t, 1, pausedRecordingRules)
} else {
require.Equal(t, 0, pausedRecordingRules)
}
if tc.recordingPaused {
require.Equal(t, 1, pausedRecordingRules)
} else {
require.Equal(t, 0, pausedRecordingRules)
}
if tc.alertPaused {
require.Equal(t, 2, pausedAlertRules)
} else {
require.Equal(t, 0, pausedAlertRules)
}
})
}
if tc.alertPaused {
require.Equal(t, 2, pausedAlertRules)
} else {
require.Equal(t, 0, pausedAlertRules)
}
})
}
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
+37 -6
View File
@@ -254,7 +254,8 @@ func convertGettableGrafanaRuleToPostable(gettable *apimodels.GettableGrafanaRul
}
type apiClient struct {
url string
url string
prometheusConversionUseLokiPaths bool
}
type LegacyApiClient struct {
@@ -1121,7 +1122,13 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle,
func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil)
path := "%s/api/convert/prometheus/config/v1/rules/%s/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s/%s"
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
@@ -1130,7 +1137,13 @@ func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTit
func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil)
path := "%s/api/convert/prometheus/config/v1/rules/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s"
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle), nil)
require.NoError(t, err)
ns, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
@@ -1139,7 +1152,13 @@ func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTit
func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimodels.PrometheusRuleGroup {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules", a.url), nil)
path := "%s/api/convert/prometheus/config/v1/rules"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules"
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url), nil)
require.NoError(t, err)
result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
@@ -1148,7 +1167,13 @@ func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimo
func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) {
t.Helper()
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil)
path := "%s/api/convert/prometheus/config/v1/rules/%s/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s/%s"
}
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
@@ -1156,7 +1181,13 @@ func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle
func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) {
t.Helper()
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil)
path := "%s/api/convert/prometheus/config/v1/rules/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s"
}
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
+1
View File
@@ -22771,6 +22771,7 @@
}
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"type": "array",
"items": {
"type": "object",
+1
View File
@@ -12838,6 +12838,7 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/components/schemas/gettableAlert"
},