diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index ff6fadee3b4..24b269cdfa5 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -513,6 +513,10 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup( 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 // is present but invalid. If the header is not present, returns (false, nil). func parseBooleanHeader(header string, headerName string) (bool, error) { diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 78a6bd71b62..9eccfa36d2e 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -147,6 +147,9 @@ func (api *API) authorize(method, path string) web.Handler { ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus), ) + case http.MethodPost + "/api/convert/api/v1/alerts": + eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite) + // Alert Instances and Silences // Silences for Grafana paths. diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 13b0f24c36c..0e33d42526e 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 63) + require.Len(t, paths, 64) ac := acmock.New() api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()} diff --git a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go index f83a31c0417..a414823d62e 100644 --- a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go @@ -31,6 +31,7 @@ type ConvertPrometheusApi interface { RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusPostAlertmanagerConfig(*contextmodel.ReqContext) response.Response RouteConvertPrometheusPostRuleGroup(*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 { 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 { // Parse Path Parameters namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] @@ -248,6 +252,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, 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( toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go index 13547093ca1..332f2b39f14 100644 --- a/pkg/services/ngalert/api/prometheus_conversion.go +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -15,6 +15,38 @@ import ( 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 { svc *ConvertPrometheusSrv } @@ -47,75 +79,18 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRuleGroup(c } 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 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) - } - } - - 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)) + if err := parseJSONOrYAML(ctx, &promGroup); err != nil { + return errorToResponse(err) } return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup) } 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 - - 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, &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)) + if err := parseJSONOrYAML(ctx, &promNamespaces); err != nil { + return errorToResponse(err) } return f.svc.RouteConvertPrometheusPostRuleGroups(ctx, promNamespaces) @@ -149,3 +124,13 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRule func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response { 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) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 6b26ef64ae9..84979f94fb4 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -576,6 +576,14 @@ }, "type": "object" }, + "AlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/Config" + } + }, + "type": "object" + }, "ApiRuleNode": { "properties": { "alert": { @@ -3666,7 +3674,6 @@ "type": "object" }, "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": { "active_time_intervals": { "items": { @@ -3708,12 +3715,6 @@ }, "type": "array" }, - "object_matchers": { - "$ref": "#/definitions/ObjectMatchers" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, "receiver": { "type": "string" }, @@ -3727,6 +3728,7 @@ "type": "array" } }, + "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "RouteExport": { diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index 0dd840a1d17..813c99e8cbc 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -1,6 +1,7 @@ package definitions import ( + "github.com/prometheus/alertmanager/config" "github.com/prometheus/common/model" ) @@ -200,6 +201,21 @@ import ( // 202: ConvertPrometheusResponse // 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 type RouteConvertPrometheusPostRuleGroupParams struct { // in: path @@ -267,3 +283,14 @@ type ConvertPrometheusResponse struct { ErrorType string `json:"errorType"` 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"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index f8e00281d7e..455cf5cb50c 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -576,6 +576,14 @@ }, "type": "object" }, + "AlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/Config" + } + }, + "type": "object" + }, "ApiRuleNode": { "properties": { "alert": { @@ -3666,7 +3674,6 @@ "type": "object" }, "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": { "active_time_intervals": { "items": { @@ -3708,12 +3715,6 @@ }, "type": "array" }, - "object_matchers": { - "$ref": "#/definitions/ObjectMatchers" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, "receiver": { "type": "string" }, @@ -3727,6 +3728,7 @@ "type": "array" } }, + "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "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": { "get": { "operationId": "RouteConvertPrometheusGetRules", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 9059e95d64f..f40362f55dd 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -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": { "get": { "produces": [ @@ -4747,6 +4783,14 @@ } } }, + "AlertmanagerUserConfig": { + "type": "object", + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/Config" + } + } + }, "ApiRuleNode": { "type": "object", "properties": { @@ -7839,8 +7883,8 @@ } }, "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", + "title": "A Route is a node that contains definitions of how to handle alerts.", "properties": { "active_time_intervals": { "type": "array", @@ -7882,12 +7926,6 @@ "type": "string" } }, - "object_matchers": { - "$ref": "#/definitions/ObjectMatchers" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, "receiver": { "type": "string" }, diff --git a/public/api-merged.json b/public/api-merged.json index 16cfd496227..7b2a63632a9 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12994,6 +12994,14 @@ } } }, + "AlertmanagerUserConfig": { + "type": "object", + "properties": { + "alertmanager_config": { + "$ref": "#/definitions/Config" + } + } + }, "Annotation": { "type": "object", "properties": { @@ -20019,8 +20027,8 @@ } }, "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", + "title": "A Route is a node that contains definitions of how to handle alerts.", "properties": { "active_time_intervals": { "type": "array", @@ -20062,12 +20070,6 @@ "type": "string" } }, - "object_matchers": { - "$ref": "#/definitions/ObjectMatchers" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, "receiver": { "type": "string" }, diff --git a/public/openapi3.json b/public/openapi3.json index e8784d26092..1c59dd4fe75 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -3044,6 +3044,14 @@ }, "type": "object" }, + "AlertmanagerUserConfig": { + "properties": { + "alertmanager_config": { + "$ref": "#/components/schemas/Config" + } + }, + "type": "object" + }, "Annotation": { "properties": { "alertId": { @@ -10069,7 +10077,6 @@ "type": "object" }, "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": { "active_time_intervals": { "items": { @@ -10111,12 +10118,6 @@ }, "type": "array" }, - "object_matchers": { - "$ref": "#/components/schemas/ObjectMatchers" - }, - "provenance": { - "$ref": "#/components/schemas/Provenance" - }, "receiver": { "type": "string" }, @@ -10130,6 +10131,7 @@ "type": "array" } }, + "title": "A Route is a node that contains definitions of how to handle alerts.", "type": "object" }, "RouteExport": {