Alerting: Empty endpoint to load alertmanager config with mimirtool (#106266)

This commit is contained in:
Alexander Akhmetov 2025-06-10 11:35:57 +02:00 committed by GitHub
parent 0da0fb5af1
commit a4fa8ab891
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 214 additions and 97 deletions

View File

@ -513,6 +513,10 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
return grafanaGroup, nil return grafanaGroup, nil
} }
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, config apimodels.AlertmanagerUserConfig) response.Response {
return response.Error(501, "Not implemented", nil)
}
// parseBooleanHeader parses a boolean header value, returning an error if the header // parseBooleanHeader parses a boolean header value, returning an error if the header
// is present but invalid. If the header is not present, returns (false, nil). // is present but invalid. If the header is not present, returns (false, nil).
func parseBooleanHeader(header string, headerName string) (bool, error) { func parseBooleanHeader(header string, headerName string) (bool, error) {

View File

@ -147,6 +147,9 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus), ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
) )
case http.MethodPost + "/api/convert/api/v1/alerts":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)
// Alert Instances and Silences // Alert Instances and Silences
// Silences for Grafana paths. // Silences for Grafana paths.

View File

@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) {
} }
paths[p] = methods paths[p] = methods
} }
require.Len(t, paths, 63) require.Len(t, paths, 64)
ac := acmock.New() ac := acmock.New()
api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()} api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()}

View File

@ -31,6 +31,7 @@ type ConvertPrometheusApi interface {
RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusPostAlertmanagerConfig(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusPostRuleGroups(*contextmodel.ReqContext) response.Response RouteConvertPrometheusPostRuleGroups(*contextmodel.ReqContext) response.Response
} }
@ -93,6 +94,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetRuleGroup(ctx *co
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response { func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusGetRules(ctx) return f.handleRouteConvertPrometheusGetRules(ctx)
} }
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusPostAlertmanagerConfig(ctx)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *contextmodel.ReqContext) response.Response { func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters // Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@ -248,6 +252,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
m, m,
), ),
) )
group.Post(
toMacaronPath("/api/convert/api/v1/alerts"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/convert/api/v1/alerts"),
metrics.Instrument(
http.MethodPost,
"/api/convert/api/v1/alerts",
api.Hooks.Wrap(srv.RouteConvertPrometheusPostAlertmanagerConfig),
m,
),
)
group.Post( group.Post(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting), requestmeta.SetOwner(requestmeta.TeamAlerting),

View File

@ -15,6 +15,38 @@ import (
var errorUnsupportedMediaType = errutil.UnsupportedMediaType("alerting.unsupportedMediaType") var errorUnsupportedMediaType = errutil.UnsupportedMediaType("alerting.unsupportedMediaType")
// parseJSONOrYAML unmarshals body into target based on content-type, defaulting to YAML
func parseJSONOrYAML(ctx *contextmodel.ReqContext, target interface{}) error {
var m string
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
return err
}
defer func() { _ = ctx.Req.Body.Close() }()
contentType := ctx.Req.Header.Get("content-type")
// Parse content-type only if it's not empty,
// otherwise we'll assume it's yaml
if contentType != "" {
m, _, err = mime.ParseMediaType(contentType)
if err != nil {
return err
}
}
switch m {
case "application/yaml", "":
// mimirtool does not send content-type, so if it's empty, we assume it's yaml
return yaml.Unmarshal(body, target)
case "application/json":
return json.Unmarshal(body, target)
default:
return errorUnsupportedMediaType.Errorf("unsupported media type: %s, only application/yaml and application/json are supported", m)
}
}
type ConvertPrometheusApiHandler struct { type ConvertPrometheusApiHandler struct {
svc *ConvertPrometheusSrv svc *ConvertPrometheusSrv
} }
@ -47,75 +79,18 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRuleGroup(c
} }
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
return errorToResponse(err)
}
defer func() { _ = ctx.Req.Body.Close() }()
var promGroup apimodels.PrometheusRuleGroup var promGroup apimodels.PrometheusRuleGroup
var m string if err := parseJSONOrYAML(ctx, &promGroup); err != nil {
return errorToResponse(err)
// Parse content-type only if it's not empty,
// otherwise we'll assume it's yaml
contentType := ctx.Req.Header.Get("content-type")
if contentType != "" {
m, _, err = mime.ParseMediaType(contentType)
if err != nil {
return errorToResponse(err)
}
}
switch m {
case "application/yaml", "":
// mimirtool does not send content-type, so if it's empty, we assume it's yaml
if err := yaml.Unmarshal(body, &promGroup); err != nil {
return errorToResponse(err)
}
case "application/json":
if err := json.Unmarshal(body, &promGroup); err != nil {
return errorToResponse(err)
}
default:
return errorToResponse(errorUnsupportedMediaType.Errorf("unsupported media type: %s, only application/yaml and application/json are supported", m))
} }
return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup) return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup)
} }
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroups(ctx *contextmodel.ReqContext) response.Response { func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
return errorToResponse(err)
}
defer func() { _ = ctx.Req.Body.Close() }()
var m string
// Parse content-type only if it's not empty,
// otherwise we'll assume it's yaml
contentType := ctx.Req.Header.Get("content-type")
if contentType != "" {
m, _, err = mime.ParseMediaType(contentType)
if err != nil {
return errorToResponse(err)
}
}
var promNamespaces map[string][]apimodels.PrometheusRuleGroup var promNamespaces map[string][]apimodels.PrometheusRuleGroup
if err := parseJSONOrYAML(ctx, &promNamespaces); err != nil {
switch m { return errorToResponse(err)
case "application/yaml", "":
// mimirtool does not send content-type, so if it's empty, we assume it's yaml
if err := yaml.Unmarshal(body, &promNamespaces); err != nil {
return errorToResponse(err)
}
case "application/json":
if err := json.Unmarshal(body, &promNamespaces); err != nil {
return errorToResponse(err)
}
default:
return errorToResponse(errorUnsupportedMediaType.Errorf("unsupported media type: %s, only application/yaml and application/json are supported", m))
} }
return f.svc.RouteConvertPrometheusPostRuleGroups(ctx, promNamespaces) return f.svc.RouteConvertPrometheusPostRuleGroups(ctx, promNamespaces)
@ -149,3 +124,13 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRule
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response { func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusPostRuleGroups(ctx) return f.handleRouteConvertPrometheusPostRuleGroups(ctx)
} }
// alertmanager
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
var config apimodels.AlertmanagerUserConfig
if err := parseJSONOrYAML(ctx, &config); err != nil {
return errorToResponse(err)
}
return f.svc.RouteConvertPrometheusPostAlertmanagerConfig(ctx, config)
}

View File

@ -576,6 +576,14 @@
}, },
"type": "object" "type": "object"
}, },
"AlertmanagerUserConfig": {
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
}
},
"type": "object"
},
"ApiRuleNode": { "ApiRuleNode": {
"properties": { "properties": {
"alert": { "alert": {
@ -3666,7 +3674,6 @@
"type": "object" "type": "object"
}, },
"Route": { "Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": { "properties": {
"active_time_intervals": { "active_time_intervals": {
"items": { "items": {
@ -3708,12 +3715,6 @@
}, },
"type": "array" "type": "array"
}, },
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": { "receiver": {
"type": "string" "type": "string"
}, },
@ -3727,6 +3728,7 @@
"type": "array" "type": "array"
} }
}, },
"title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object" "type": "object"
}, },
"RouteExport": { "RouteExport": {

View File

@ -1,6 +1,7 @@
package definitions package definitions
import ( import (
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
) )
@ -200,6 +201,21 @@ import (
// 202: ConvertPrometheusResponse // 202: ConvertPrometheusResponse
// 403: ForbiddenError // 403: ForbiddenError
// Route for `mimirtool alertmanager load`
// swagger:route POST /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusPostAlertmanagerConfig
//
// Load Alertmanager configuration to Grafana and merge it with the existing configuration.
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
//
// Extensions:
// x-raw-request: true
// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup // swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup
type RouteConvertPrometheusPostRuleGroupParams struct { type RouteConvertPrometheusPostRuleGroupParams struct {
// in: path // in: path
@ -267,3 +283,14 @@ type ConvertPrometheusResponse struct {
ErrorType string `json:"errorType"` ErrorType string `json:"errorType"`
Error string `json:"error"` Error string `json:"error"`
} }
// swagger:parameters RouteConvertPrometheusPostAlertmanagerConfig
type RouteConvertPrometheusPostAlertmanagerConfigParams struct {
// in:body
Body AlertmanagerUserConfig
}
// swagger:model
type AlertmanagerUserConfig struct {
AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"`
}

View File

@ -576,6 +576,14 @@
}, },
"type": "object" "type": "object"
}, },
"AlertmanagerUserConfig": {
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
}
},
"type": "object"
},
"ApiRuleNode": { "ApiRuleNode": {
"properties": { "properties": {
"alert": { "alert": {
@ -3666,7 +3674,6 @@
"type": "object" "type": "object"
}, },
"Route": { "Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": { "properties": {
"active_time_intervals": { "active_time_intervals": {
"items": { "items": {
@ -3708,12 +3715,6 @@
}, },
"type": "array" "type": "array"
}, },
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": { "receiver": {
"type": "string" "type": "string"
}, },
@ -3727,6 +3728,7 @@
"type": "array" "type": "array"
} }
}, },
"title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object" "type": "object"
}, },
"RouteExport": { "RouteExport": {
@ -6820,6 +6822,42 @@
] ]
} }
}, },
"/convert/api/v1/alerts": {
"post": {
"operationId": "RouteConvertPrometheusPostAlertmanagerConfig",
"parameters": [
{
"in": "body",
"name": "Body",
"schema": {
"$ref": "#/definitions/AlertmanagerUserConfig"
}
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.",
"tags": [
"convert_prometheus"
],
"x-raw-request": "true"
}
},
"/convert/prometheus/config/v1/rules": { "/convert/prometheus/config/v1/rules": {
"get": { "get": {
"operationId": "RouteConvertPrometheusGetRules", "operationId": "RouteConvertPrometheusGetRules",

View File

@ -1360,6 +1360,42 @@
} }
} }
}, },
"/convert/api/v1/alerts": {
"post": {
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.",
"operationId": "RouteConvertPrometheusPostAlertmanagerConfig",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/AlertmanagerUserConfig"
}
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"x-raw-request": "true"
}
},
"/convert/prometheus/config/v1/rules": { "/convert/prometheus/config/v1/rules": {
"get": { "get": {
"produces": [ "produces": [
@ -4747,6 +4783,14 @@
} }
} }
}, },
"AlertmanagerUserConfig": {
"type": "object",
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
}
}
},
"ApiRuleNode": { "ApiRuleNode": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7839,8 +7883,8 @@
} }
}, },
"Route": { "Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object", "type": "object",
"title": "A Route is a node that contains definitions of how to handle alerts.",
"properties": { "properties": {
"active_time_intervals": { "active_time_intervals": {
"type": "array", "type": "array",
@ -7882,12 +7926,6 @@
"type": "string" "type": "string"
} }
}, },
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": { "receiver": {
"type": "string" "type": "string"
}, },

View File

@ -12994,6 +12994,14 @@
} }
} }
}, },
"AlertmanagerUserConfig": {
"type": "object",
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
}
}
},
"Annotation": { "Annotation": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -20019,8 +20027,8 @@
} }
}, },
"Route": { "Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object", "type": "object",
"title": "A Route is a node that contains definitions of how to handle alerts.",
"properties": { "properties": {
"active_time_intervals": { "active_time_intervals": {
"type": "array", "type": "array",
@ -20062,12 +20070,6 @@
"type": "string" "type": "string"
} }
}, },
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": { "receiver": {
"type": "string" "type": "string"
}, },

View File

@ -3044,6 +3044,14 @@
}, },
"type": "object" "type": "object"
}, },
"AlertmanagerUserConfig": {
"properties": {
"alertmanager_config": {
"$ref": "#/components/schemas/Config"
}
},
"type": "object"
},
"Annotation": { "Annotation": {
"properties": { "properties": {
"alertId": { "alertId": {
@ -10069,7 +10077,6 @@
"type": "object" "type": "object"
}, },
"Route": { "Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": { "properties": {
"active_time_intervals": { "active_time_intervals": {
"items": { "items": {
@ -10111,12 +10118,6 @@
}, },
"type": "array" "type": "array"
}, },
"object_matchers": {
"$ref": "#/components/schemas/ObjectMatchers"
},
"provenance": {
"$ref": "#/components/schemas/Provenance"
},
"receiver": { "receiver": {
"type": "string" "type": "string"
}, },
@ -10130,6 +10131,7 @@
"type": "array" "type": "array"
} }
}, },
"title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object" "type": "object"
}, },
"RouteExport": { "RouteExport": {