mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 10:18:29 +08:00
Alerting: Fetch all applied alerting configurations (#65728)
* WIP * skip invalid historic configurations instead of erroring * add warning log when bad historic config is found * remove unused custom marshaller for GettableHistoricUserConfig * add id to historic user config, move limit check to store, fix typo * swagger spec
This commit is contained in:
@ -133,6 +133,16 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) re
|
||||
return response.JSON(http.StatusOK, config)
|
||||
}
|
||||
|
||||
func (srv AlertmanagerSrv) RouteGetAlertingConfigHistory(c *contextmodel.ReqContext) response.Response {
|
||||
limit := c.QueryInt("limit")
|
||||
configs, err := srv.mam.GetAppliedAlertmanagerConfigurations(c.Req.Context(), c.OrgID, limit)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, err.Error())
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, configs)
|
||||
}
|
||||
|
||||
func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *contextmodel.ReqContext) response.Response {
|
||||
am, errResp := srv.AlertmanagerFor(c.OrgID)
|
||||
if errResp != nil {
|
||||
|
@ -291,6 +291,96 @@ func TestAlertmanagerConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteGetAlertingConfigHistory(t *testing.T) {
|
||||
sut := createSut(t, nil)
|
||||
|
||||
t.Run("assert 200 and empty slice when no applied configurations are found", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
q.Add("limit", "10")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(10)
|
||||
|
||||
response := sut.RouteGetAlertingConfigHistory(rc)
|
||||
require.Equal(tt, 200, response.Status())
|
||||
|
||||
var configs []apimodels.GettableHistoricUserConfig
|
||||
err = json.Unmarshal(response.Body(), &configs)
|
||||
require.NoError(tt, err)
|
||||
|
||||
require.Len(tt, configs, 0)
|
||||
})
|
||||
|
||||
t.Run("assert 200 and one config in the response for an org that has one successfully applied configuration", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
q.Add("limit", "10")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(1)
|
||||
|
||||
response := sut.RouteGetAlertingConfigHistory(rc)
|
||||
require.Equal(tt, 200, response.Status())
|
||||
|
||||
var configs []apimodels.GettableHistoricUserConfig
|
||||
err = json.Unmarshal(response.Body(), &configs)
|
||||
require.NoError(tt, err)
|
||||
|
||||
require.Len(tt, configs, 1)
|
||||
})
|
||||
|
||||
t.Run("assert 200 when no limit is provided", func(tt *testing.T) {
|
||||
rc := createRequestCtxInOrg(1)
|
||||
|
||||
response := sut.RouteGetAlertingConfigHistory(rc)
|
||||
require.Equal(tt, 200, response.Status())
|
||||
|
||||
configs := asGettableHistoricUserConfigs(tt, response)
|
||||
for _, config := range configs {
|
||||
require.NotZero(tt, config.LastApplied)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assert 200 when limit is < 1", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
q.Add("limit", "0")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(1)
|
||||
|
||||
response := sut.RouteGetAlertingConfigHistory(rc)
|
||||
require.Equal(tt, 200, response.Status())
|
||||
|
||||
configs := asGettableHistoricUserConfigs(tt, response)
|
||||
for _, config := range configs {
|
||||
require.NotZero(tt, config.LastApplied)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assert 200 when limit is > 100", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
q.Add("limit", "1000")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(1)
|
||||
|
||||
response := sut.RouteGetAlertingConfigHistory(rc)
|
||||
require.Equal(tt, 200, response.Status())
|
||||
|
||||
configs := asGettableHistoricUserConfigs(tt, response)
|
||||
for _, config := range configs {
|
||||
require.NotZero(tt, config.LastApplied)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSilenceCreate(t *testing.T) {
|
||||
makeSilence := func(comment string, createdBy string,
|
||||
startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence {
|
||||
@ -661,3 +751,11 @@ func asGettableUserConfig(t *testing.T, r response.Response) *apimodels.Gettable
|
||||
require.NoError(t, err)
|
||||
return body
|
||||
}
|
||||
|
||||
func asGettableHistoricUserConfigs(t *testing.T, r response.Response) []apimodels.GettableHistoricUserConfig {
|
||||
t.Helper()
|
||||
var body []apimodels.GettableHistoricUserConfig
|
||||
err := json.Unmarshal(r.Body(), &body)
|
||||
require.NoError(t, err)
|
||||
return body
|
||||
}
|
||||
|
@ -158,6 +158,9 @@ func (api *API) authorize(method, path string) web.Handler {
|
||||
case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/alerts":
|
||||
fallback = middleware.ReqEditorRole
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
|
||||
case http.MethodGet + "/api/alertmanager/grafana/config/history":
|
||||
fallback = middleware.ReqEditorRole
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
|
||||
case http.MethodGet + "/api/alertmanager/grafana/api/v2/status":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
|
||||
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/alerts":
|
||||
|
@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) {
|
||||
}
|
||||
paths[p] = methods
|
||||
}
|
||||
require.Len(t, paths, 45)
|
||||
require.Len(t, paths, 46)
|
||||
|
||||
ac := acmock.New()
|
||||
api := &API{AccessControl: ac}
|
||||
|
@ -159,6 +159,10 @@ func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *contex
|
||||
return f.GrafanaSvc.RouteGetAlertingConfig(ctx)
|
||||
}
|
||||
|
||||
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfigHistory(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.GrafanaSvc.RouteGetAlertingConfigHistory(ctx)
|
||||
}
|
||||
|
||||
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *contextmodel.ReqContext, id string) response.Response {
|
||||
return f.GrafanaSvc.RouteGetSilence(ctx, id)
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ type AlertmanagerApi interface {
|
||||
RouteGetGrafanaAMAlerts(*contextmodel.ReqContext) response.Response
|
||||
RouteGetGrafanaAMStatus(*contextmodel.ReqContext) response.Response
|
||||
RouteGetGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response
|
||||
RouteGetGrafanaAlertingConfigHistory(*contextmodel.ReqContext) response.Response
|
||||
RouteGetGrafanaReceivers(*contextmodel.ReqContext) response.Response
|
||||
RouteGetGrafanaSilence(*contextmodel.ReqContext) response.Response
|
||||
RouteGetGrafanaSilences(*contextmodel.ReqContext) response.Response
|
||||
@ -113,6 +114,9 @@ func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *contextmodel.ReqCo
|
||||
func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteGetGrafanaAlertingConfig(ctx)
|
||||
}
|
||||
func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfigHistory(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteGetGrafanaAlertingConfigHistory(ctx)
|
||||
}
|
||||
func (f *AlertmanagerApiHandler) RouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response {
|
||||
return f.handleRouteGetGrafanaReceivers(ctx)
|
||||
}
|
||||
@ -314,6 +318,16 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/alertmanager/grafana/config/history"),
|
||||
api.authorize(http.MethodGet, "/api/alertmanager/grafana/config/history"),
|
||||
metrics.Instrument(
|
||||
http.MethodGet,
|
||||
"/api/alertmanager/grafana/config/history",
|
||||
srv.RouteGetGrafanaAlertingConfigHistory,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/alertmanager/grafana/config/api/v1/receivers"),
|
||||
api.authorize(http.MethodGet, "/api/alertmanager/grafana/config/api/v1/receivers"),
|
||||
|
@ -185,9 +185,9 @@
|
||||
},
|
||||
"execErrState": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"OK"
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@ -528,6 +528,9 @@
|
||||
"DataLink": {
|
||||
"description": "DataLink define what",
|
||||
"properties": {
|
||||
"internal": {
|
||||
"$ref": "#/definitions/InternalDataLink"
|
||||
},
|
||||
"targetBlank": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@ -715,6 +718,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"EnumFieldConfig": {
|
||||
"description": "Enum field config\nVector values are used as lookup keys into the enum fields",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Color is the color value for a given index (empty is undefined)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of the enum state",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"icon": {
|
||||
"description": "Icon supports setting an icon for a given index value",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"description": "Value is the string display value for a given index",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorType": {
|
||||
"title": "ErrorType models the different API error types.",
|
||||
"type": "string"
|
||||
@ -754,6 +791,9 @@
|
||||
"type": "object"
|
||||
},
|
||||
"EvalQueriesResponse": {},
|
||||
"ExplorePanelsState": {
|
||||
"description": "This is an object constructed with the keys as the values of the enum VisType and the value being a bag of properties"
|
||||
},
|
||||
"ExtendedReceiver": {
|
||||
"properties": {
|
||||
"email_configs": {
|
||||
@ -870,6 +910,9 @@
|
||||
"thresholds": {
|
||||
"$ref": "#/definitions/ThresholdsConfig"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FieldTypeConfig"
|
||||
},
|
||||
"unit": {
|
||||
"description": "Numeric Options",
|
||||
"type": "string"
|
||||
@ -882,11 +925,20 @@
|
||||
"title": "FieldConfig represents the display properties for a Field.",
|
||||
"type": "object"
|
||||
},
|
||||
"FieldTypeConfig": {
|
||||
"description": "FieldTypeConfig has type specific configs, only one should be active at a time",
|
||||
"properties": {
|
||||
"enum": {
|
||||
"$ref": "#/definitions/EnumFieldConfig"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Frame": {
|
||||
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
|
||||
"properties": {
|
||||
"Fields": {
|
||||
"description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.",
|
||||
"description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.\nThere should be no `nil` entries in the Fields slice (making them pointers was a mistake).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Field"
|
||||
},
|
||||
@ -958,6 +1010,9 @@
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FrameType"
|
||||
},
|
||||
"typeVersion": {
|
||||
"$ref": "#/definitions/FrameTypeVersion"
|
||||
}
|
||||
},
|
||||
"title": "FrameMeta matches:",
|
||||
@ -967,8 +1022,16 @@
|
||||
"description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.",
|
||||
"type": "string"
|
||||
},
|
||||
"FrameTypeVersion": {
|
||||
"items": {
|
||||
"format": "uint64",
|
||||
"type": "integer"
|
||||
},
|
||||
"title": "FrameType is a 2 number version (Major / Minor).",
|
||||
"type": "array"
|
||||
},
|
||||
"Frames": {
|
||||
"description": "It is the main data container within a backend.DataResponse.",
|
||||
"description": "It is the main data container within a backend.DataResponse.\nThere should be no `nil` entries in the Frames slice (making them pointers was a mistake).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Frame"
|
||||
},
|
||||
@ -1259,6 +1322,34 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"GettableHistoricUserConfig": {
|
||||
"properties": {
|
||||
"alertmanager_config": {
|
||||
"$ref": "#/definitions/GettableApiAlertingConfig"
|
||||
},
|
||||
"id": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"last_applied": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"template_file_provenances": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Provenance"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"template_files": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"GettableNGalertConfig": {
|
||||
"properties": {
|
||||
"alertmanagersChoice": {
|
||||
@ -1521,6 +1612,31 @@
|
||||
"title": "InspectType is a type for the Inspect property of a Notice.",
|
||||
"type": "integer"
|
||||
},
|
||||
"InternalDataLink": {
|
||||
"description": "InternalDataLink definition to allow Explore links to be constructed in the backend",
|
||||
"properties": {
|
||||
"datasourceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"datasourceUid": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelsState": {
|
||||
"$ref": "#/definitions/ExplorePanelsState"
|
||||
},
|
||||
"query": {},
|
||||
"timeRange": {
|
||||
"$ref": "#/definitions/TimeRange"
|
||||
},
|
||||
"transformations": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/LinkTransformationConfig"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Json": {
|
||||
"type": "object"
|
||||
},
|
||||
@ -1562,6 +1678,23 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"LinkTransformationConfig": {
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/SupportedTransformationTypes"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MatchRegexps": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Regexp"
|
||||
@ -2272,9 +2405,9 @@
|
||||
},
|
||||
"execErrState": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"OK"
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@ -2472,6 +2605,9 @@
|
||||
"thresholds": {
|
||||
"$ref": "#/definitions/ThresholdsConfig"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FieldTypeConfig"
|
||||
},
|
||||
"unit": {
|
||||
"description": "Numeric Options",
|
||||
"type": "string"
|
||||
@ -3040,6 +3176,9 @@
|
||||
"Success": {
|
||||
"$ref": "#/definitions/ResponseDetails"
|
||||
},
|
||||
"SupportedTransformationTypes": {
|
||||
"type": "string"
|
||||
},
|
||||
"TLSConfig": {
|
||||
"properties": {
|
||||
"ca_file": {
|
||||
@ -3279,22 +3418,20 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TimeRange": {
|
||||
"description": "For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.",
|
||||
"description": "Redefining this to avoid an import cycle",
|
||||
"properties": {
|
||||
"EndMinute": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"from": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"StartMinute": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"to": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.",
|
||||
"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\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 RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
@ -3330,7 +3467,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"
|
||||
},
|
||||
"Userinfo": {
|
||||
@ -3530,7 +3667,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
@ -3894,6 +4030,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3931,6 +4068,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"properties": {
|
||||
"active": {
|
||||
"description": "active",
|
||||
@ -4864,6 +5002,15 @@
|
||||
"application/json"
|
||||
],
|
||||
"responses": {
|
||||
"GettableHistoricUserConfigs": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableHistoricUserConfig"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"receiversResponse": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
|
@ -54,6 +54,13 @@ import (
|
||||
// 400: ValidationError
|
||||
// 404: NotFound
|
||||
|
||||
// swagger:route GET /api/alertmanager/grafana/config/history alertmanager RouteGetGrafanaAlertingConfigHistory
|
||||
//
|
||||
// gets Alerting configurations that were successfully applied in the past
|
||||
//
|
||||
// Responses:
|
||||
// 200: GettableHistoricUserConfigs
|
||||
|
||||
// swagger:route DELETE /api/alertmanager/grafana/config/api/v1/alerts alertmanager RouteDeleteGrafanaAlertingConfig
|
||||
//
|
||||
// deletes the Alerting config for a tenant
|
||||
@ -229,6 +236,13 @@ type AlertManagerNotReady struct{}
|
||||
// swagger:model
|
||||
type MultiStatus struct{}
|
||||
|
||||
// swagger:parameters RouteGetGrafanaAlertingConfigHistory
|
||||
type RouteGetGrafanaAlertingConfigHistoryParams struct {
|
||||
// Limit response to n historic configurations.
|
||||
// in:query
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// swagger:parameters RoutePostTestGrafanaReceivers
|
||||
type TestReceiversConfigParams struct {
|
||||
// in:body
|
||||
@ -633,6 +647,20 @@ func (c *GettableUserConfig) GetGrafanaReceiverMap() map[string]*GettableGrafana
|
||||
return UIDs
|
||||
}
|
||||
|
||||
type GettableHistoricUserConfig struct {
|
||||
ID int64 `yaml:"id" json:"id"`
|
||||
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
|
||||
TemplateFileProvenances map[string]Provenance `yaml:"template_file_provenances,omitempty" json:"template_file_provenances,omitempty"`
|
||||
AlertmanagerConfig GettableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||
LastApplied *strfmt.DateTime `yaml:"last_applied,omitempty" json:"last_applied,omitempty"`
|
||||
}
|
||||
|
||||
// swagger:response GettableHistoricUserConfigs
|
||||
type GettableHistoricUserConfigs struct {
|
||||
// in:body
|
||||
Body []GettableHistoricUserConfig
|
||||
}
|
||||
|
||||
type GettableApiAlertingConfig struct {
|
||||
Config `yaml:",inline"`
|
||||
MuteTimeProvenances map[string]Provenance `yaml:"muteTimeProvenances,omitempty" json:"muteTimeProvenances,omitempty"`
|
||||
|
@ -185,9 +185,9 @@
|
||||
},
|
||||
"execErrState": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"OK"
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@ -528,6 +528,9 @@
|
||||
"DataLink": {
|
||||
"description": "DataLink define what",
|
||||
"properties": {
|
||||
"internal": {
|
||||
"$ref": "#/definitions/InternalDataLink"
|
||||
},
|
||||
"targetBlank": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@ -715,6 +718,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"EnumFieldConfig": {
|
||||
"description": "Enum field config\nVector values are used as lookup keys into the enum fields",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Color is the color value for a given index (empty is undefined)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of the enum state",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"icon": {
|
||||
"description": "Icon supports setting an icon for a given index value",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"description": "Value is the string display value for a given index",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorType": {
|
||||
"title": "ErrorType models the different API error types.",
|
||||
"type": "string"
|
||||
@ -754,6 +791,9 @@
|
||||
"type": "object"
|
||||
},
|
||||
"EvalQueriesResponse": {},
|
||||
"ExplorePanelsState": {
|
||||
"description": "This is an object constructed with the keys as the values of the enum VisType and the value being a bag of properties"
|
||||
},
|
||||
"ExtendedReceiver": {
|
||||
"properties": {
|
||||
"email_configs": {
|
||||
@ -870,6 +910,9 @@
|
||||
"thresholds": {
|
||||
"$ref": "#/definitions/ThresholdsConfig"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FieldTypeConfig"
|
||||
},
|
||||
"unit": {
|
||||
"description": "Numeric Options",
|
||||
"type": "string"
|
||||
@ -882,11 +925,20 @@
|
||||
"title": "FieldConfig represents the display properties for a Field.",
|
||||
"type": "object"
|
||||
},
|
||||
"FieldTypeConfig": {
|
||||
"description": "FieldTypeConfig has type specific configs, only one should be active at a time",
|
||||
"properties": {
|
||||
"enum": {
|
||||
"$ref": "#/definitions/EnumFieldConfig"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Frame": {
|
||||
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
|
||||
"properties": {
|
||||
"Fields": {
|
||||
"description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.",
|
||||
"description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.\nThere should be no `nil` entries in the Fields slice (making them pointers was a mistake).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Field"
|
||||
},
|
||||
@ -958,6 +1010,9 @@
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FrameType"
|
||||
},
|
||||
"typeVersion": {
|
||||
"$ref": "#/definitions/FrameTypeVersion"
|
||||
}
|
||||
},
|
||||
"title": "FrameMeta matches:",
|
||||
@ -967,8 +1022,16 @@
|
||||
"description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.",
|
||||
"type": "string"
|
||||
},
|
||||
"FrameTypeVersion": {
|
||||
"items": {
|
||||
"format": "uint64",
|
||||
"type": "integer"
|
||||
},
|
||||
"title": "FrameType is a 2 number version (Major / Minor).",
|
||||
"type": "array"
|
||||
},
|
||||
"Frames": {
|
||||
"description": "It is the main data container within a backend.DataResponse.",
|
||||
"description": "It is the main data container within a backend.DataResponse.\nThere should be no `nil` entries in the Frames slice (making them pointers was a mistake).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Frame"
|
||||
},
|
||||
@ -1259,6 +1322,34 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"GettableHistoricUserConfig": {
|
||||
"properties": {
|
||||
"alertmanager_config": {
|
||||
"$ref": "#/definitions/GettableApiAlertingConfig"
|
||||
},
|
||||
"id": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"last_applied": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"template_file_provenances": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Provenance"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"template_files": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"GettableNGalertConfig": {
|
||||
"properties": {
|
||||
"alertmanagersChoice": {
|
||||
@ -1521,6 +1612,31 @@
|
||||
"title": "InspectType is a type for the Inspect property of a Notice.",
|
||||
"type": "integer"
|
||||
},
|
||||
"InternalDataLink": {
|
||||
"description": "InternalDataLink definition to allow Explore links to be constructed in the backend",
|
||||
"properties": {
|
||||
"datasourceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"datasourceUid": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelsState": {
|
||||
"$ref": "#/definitions/ExplorePanelsState"
|
||||
},
|
||||
"query": {},
|
||||
"timeRange": {
|
||||
"$ref": "#/definitions/TimeRange"
|
||||
},
|
||||
"transformations": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/LinkTransformationConfig"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Json": {
|
||||
"type": "object"
|
||||
},
|
||||
@ -1562,6 +1678,23 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"LinkTransformationConfig": {
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/SupportedTransformationTypes"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MatchRegexps": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Regexp"
|
||||
@ -2272,9 +2405,9 @@
|
||||
},
|
||||
"execErrState": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"OK"
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@ -2472,6 +2605,9 @@
|
||||
"thresholds": {
|
||||
"$ref": "#/definitions/ThresholdsConfig"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FieldTypeConfig"
|
||||
},
|
||||
"unit": {
|
||||
"description": "Numeric Options",
|
||||
"type": "string"
|
||||
@ -3040,6 +3176,9 @@
|
||||
"Success": {
|
||||
"$ref": "#/definitions/ResponseDetails"
|
||||
},
|
||||
"SupportedTransformationTypes": {
|
||||
"type": "string"
|
||||
},
|
||||
"TLSConfig": {
|
||||
"properties": {
|
||||
"ca_file": {
|
||||
@ -3279,22 +3418,21 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TimeRange": {
|
||||
"description": "For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.",
|
||||
"description": "Redefining this to avoid an import cycle",
|
||||
"properties": {
|
||||
"EndMinute": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"from": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"StartMinute": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"to": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.",
|
||||
"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\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 RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
|
||||
"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\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 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"
|
||||
@ -3635,7 +3773,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"$ref": "#/definitions/labelSet"
|
||||
@ -3697,7 +3834,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3746,14 +3882,12 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"properties": {
|
||||
"lastNotifyAttempt": {
|
||||
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
|
||||
@ -3897,6 +4031,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -4471,6 +4606,29 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/grafana/config/history": {
|
||||
"get": {
|
||||
"description": "gets Alerting configurations that were successfully applied in the past",
|
||||
"operationId": "RouteGetGrafanaAlertingConfigHistory",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Limit response to n historic configurations.",
|
||||
"format": "int64",
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"type": "integer"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/GettableHistoricUserConfigs"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"alertmanager"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/{DatasourceUID}/api/v2/alerts": {
|
||||
"get": {
|
||||
"description": "get alertmanager alerts",
|
||||
@ -6665,6 +6823,15 @@
|
||||
"application/json"
|
||||
],
|
||||
"responses": {
|
||||
"GettableHistoricUserConfigs": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableHistoricUserConfig"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"receiversResponse": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
@ -6685,4 +6852,4 @@
|
||||
}
|
||||
},
|
||||
"swagger": "2.0"
|
||||
}
|
||||
}
|
@ -435,6 +435,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/grafana/config/history": {
|
||||
"get": {
|
||||
"description": "gets Alerting configurations that were successfully applied in the past",
|
||||
"tags": [
|
||||
"alertmanager"
|
||||
],
|
||||
"operationId": "RouteGetGrafanaAlertingConfigHistory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Limit response to n historic configurations.",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/GettableHistoricUserConfigs"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/{DatasourceUID}/api/v2/alerts": {
|
||||
"get": {
|
||||
"description": "get alertmanager alerts",
|
||||
@ -2836,9 +2859,9 @@
|
||||
"execErrState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"OK"
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"for": {
|
||||
@ -2942,8 +2965,7 @@
|
||||
"$ref": "#/definitions/AlertRuleGroupExport"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$ref": "#/definitions/AlertingFileExport"
|
||||
}
|
||||
},
|
||||
"AlertingRule": {
|
||||
"description": "adapted from cortex",
|
||||
@ -3178,6 +3200,9 @@
|
||||
"description": "DataLink define what",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"internal": {
|
||||
"$ref": "#/definitions/InternalDataLink"
|
||||
},
|
||||
"targetBlank": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@ -3365,6 +3390,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EnumFieldConfig": {
|
||||
"description": "Enum field config\nVector values are used as lookup keys into the enum fields",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Color is the color value for a given index (empty is undefined)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of the enum state",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"description": "Icon supports setting an icon for a given index value",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"description": "Value is the string display value for a given index",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorType": {
|
||||
"type": "string",
|
||||
"title": "ErrorType models the different API error types."
|
||||
@ -3406,6 +3465,9 @@
|
||||
"EvalQueriesResponse": {
|
||||
"$ref": "#/definitions/EvalQueriesResponse"
|
||||
},
|
||||
"ExplorePanelsState": {
|
||||
"description": "This is an object constructed with the keys as the values of the enum VisType and the value being a bag of properties"
|
||||
},
|
||||
"ExtendedReceiver": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -3524,6 +3586,9 @@
|
||||
"thresholds": {
|
||||
"$ref": "#/definitions/ThresholdsConfig"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FieldTypeConfig"
|
||||
},
|
||||
"unit": {
|
||||
"description": "Numeric Options",
|
||||
"type": "string"
|
||||
@ -3534,13 +3599,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FieldTypeConfig": {
|
||||
"description": "FieldTypeConfig has type specific configs, only one should be active at a time",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enum": {
|
||||
"$ref": "#/definitions/EnumFieldConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Frame": {
|
||||
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
|
||||
"type": "object",
|
||||
"title": "Frame is a columnar data structure where each column is a Field.",
|
||||
"properties": {
|
||||
"Fields": {
|
||||
"description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.",
|
||||
"description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.\nThere should be no `nil` entries in the Fields slice (making them pointers was a mistake).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Field"
|
||||
@ -3612,6 +3686,9 @@
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FrameType"
|
||||
},
|
||||
"typeVersion": {
|
||||
"$ref": "#/definitions/FrameTypeVersion"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3619,8 +3696,16 @@
|
||||
"description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.",
|
||||
"type": "string"
|
||||
},
|
||||
"FrameTypeVersion": {
|
||||
"type": "array",
|
||||
"title": "FrameType is a 2 number version (Major / Minor).",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint64"
|
||||
}
|
||||
},
|
||||
"Frames": {
|
||||
"description": "It is the main data container within a backend.DataResponse.",
|
||||
"description": "It is the main data container within a backend.DataResponse.\nThere should be no `nil` entries in the Frames slice (making them pointers was a mistake).",
|
||||
"type": "array",
|
||||
"title": "Frames is a slice of Frame pointers.",
|
||||
"items": {
|
||||
@ -3911,6 +3996,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"GettableHistoricUserConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alertmanager_config": {
|
||||
"$ref": "#/definitions/GettableApiAlertingConfig"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"last_applied": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"template_file_provenances": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Provenance"
|
||||
}
|
||||
},
|
||||
"template_files": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GettableNGalertConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4173,6 +4286,31 @@
|
||||
"format": "int64",
|
||||
"title": "InspectType is a type for the Inspect property of a Notice."
|
||||
},
|
||||
"InternalDataLink": {
|
||||
"description": "InternalDataLink definition to allow Explore links to be constructed in the backend",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"datasourceUid": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelsState": {
|
||||
"$ref": "#/definitions/ExplorePanelsState"
|
||||
},
|
||||
"query": {},
|
||||
"timeRange": {
|
||||
"$ref": "#/definitions/TimeRange"
|
||||
},
|
||||
"transformations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/LinkTransformationConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Json": {
|
||||
"type": "object"
|
||||
},
|
||||
@ -4214,6 +4352,23 @@
|
||||
"$ref": "#/definitions/Label"
|
||||
}
|
||||
},
|
||||
"LinkTransformationConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/SupportedTransformationTypes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MatchRegexps": {
|
||||
"type": "object",
|
||||
"title": "MatchRegexps represents a map of Regexp.",
|
||||
@ -4938,9 +5093,9 @@
|
||||
"execErrState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error",
|
||||
"OK"
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"folderUID": {
|
||||
@ -5127,6 +5282,9 @@
|
||||
"thresholds": {
|
||||
"$ref": "#/definitions/ThresholdsConfig"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/FieldTypeConfig"
|
||||
},
|
||||
"unit": {
|
||||
"description": "Numeric Options",
|
||||
"type": "string"
|
||||
@ -5693,6 +5851,9 @@
|
||||
"Success": {
|
||||
"$ref": "#/definitions/ResponseDetails"
|
||||
},
|
||||
"SupportedTransformationTypes": {
|
||||
"type": "string"
|
||||
},
|
||||
"TLSConfig": {
|
||||
"type": "object",
|
||||
"title": "TLSConfig configures the options for TLS connections.",
|
||||
@ -5932,22 +6093,21 @@
|
||||
}
|
||||
},
|
||||
"TimeRange": {
|
||||
"description": "For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.",
|
||||
"description": "Redefining this to avoid an import cycle",
|
||||
"type": "object",
|
||||
"title": "TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.",
|
||||
"properties": {
|
||||
"EndMinute": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"from": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"StartMinute": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"to": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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\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 RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.",
|
||||
"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\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 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": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"properties": {
|
||||
@ -6290,7 +6450,6 @@
|
||||
}
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"labels",
|
||||
@ -6347,7 +6506,6 @@
|
||||
"$ref": "#/definitions/gettableAlert"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert"
|
||||
@ -6355,7 +6513,6 @@
|
||||
"$ref": "#/definitions/gettableAlerts"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
@ -6405,7 +6562,6 @@
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence"
|
||||
@ -6413,7 +6569,6 @@
|
||||
"$ref": "#/definitions/gettableSilences"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
@ -6558,6 +6713,7 @@
|
||||
}
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
@ -6711,6 +6867,15 @@
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"GettableHistoricUserConfigs": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableHistoricUserConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"receiversResponse": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
|
@ -16,6 +16,7 @@ type AlertConfiguration struct {
|
||||
|
||||
// HistoricAlertConfiguration represents a previously used alerting configuration.
|
||||
type HistoricAlertConfiguration struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
AlertConfiguration `xorm:"extends"`
|
||||
|
||||
// LastApplied a timestamp indicating the most recent time at which the configuration was applied to an Alertmanager, or 0 otherwise.
|
||||
@ -44,13 +45,8 @@ type MarkConfigurationAsAppliedCmd struct {
|
||||
ConfigurationHash string
|
||||
}
|
||||
|
||||
// GetAppliedConfigurationsQuery is the query for getting configurations that have been previously applied with no errors.
|
||||
type GetAppliedConfigurationsQuery struct {
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
func HistoricConfigFromAlertConfig(config AlertConfiguration) HistoricAlertConfiguration {
|
||||
// Reset the Id so it can be generated by the DB.
|
||||
// Reset the ID so it can be generated by the DB.
|
||||
config.ID = 0
|
||||
return HistoricAlertConfiguration{
|
||||
AlertConfiguration: config,
|
||||
|
@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
@ -37,18 +39,51 @@ func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Contex
|
||||
if err != nil {
|
||||
return definitions.GettableUserConfig{}, fmt.Errorf("failed to get latest configuration: %w", err)
|
||||
}
|
||||
cfg, err := Load([]byte(amConfig.AlertmanagerConfiguration))
|
||||
|
||||
return moa.gettableUserConfigFromAMConfigString(ctx, org, amConfig.AlertmanagerConfiguration)
|
||||
}
|
||||
|
||||
// GetAppliedAlertmanagerConfigurations returns the last n configurations marked as applied for a given org.
|
||||
func (moa *MultiOrgAlertmanager) GetAppliedAlertmanagerConfigurations(ctx context.Context, org int64, limit int) ([]*definitions.GettableHistoricUserConfig, error) {
|
||||
configs, err := moa.configStore.GetAppliedConfigurations(ctx, org, limit)
|
||||
if err != nil {
|
||||
return []*definitions.GettableHistoricUserConfig{}, fmt.Errorf("failed to get applied configurations: %w", err)
|
||||
}
|
||||
|
||||
gettableHistoricConfigs := make([]*definitions.GettableHistoricUserConfig, 0, len(configs))
|
||||
for _, config := range configs {
|
||||
appliedAt := strfmt.DateTime(time.Unix(config.LastApplied, 0).UTC())
|
||||
gettableConfig, err := moa.gettableUserConfigFromAMConfigString(ctx, org, config.AlertmanagerConfiguration)
|
||||
if err != nil {
|
||||
// If there are invalid records, skip them and return the valid ones.
|
||||
moa.logger.Warn("Invalid configuration found in alert configuration history table", "id", config.ID, "orgID", org)
|
||||
continue
|
||||
}
|
||||
|
||||
gettableHistoricConfig := definitions.GettableHistoricUserConfig{
|
||||
ID: config.ID,
|
||||
TemplateFiles: gettableConfig.TemplateFiles,
|
||||
TemplateFileProvenances: gettableConfig.TemplateFileProvenances,
|
||||
AlertmanagerConfig: gettableConfig.AlertmanagerConfig,
|
||||
LastApplied: &appliedAt,
|
||||
}
|
||||
gettableHistoricConfigs = append(gettableHistoricConfigs, &gettableHistoricConfig)
|
||||
}
|
||||
|
||||
return gettableHistoricConfigs, nil
|
||||
}
|
||||
|
||||
func (moa *MultiOrgAlertmanager) gettableUserConfigFromAMConfigString(ctx context.Context, orgID int64, config string) (definitions.GettableUserConfig, error) {
|
||||
cfg, err := Load([]byte(config))
|
||||
if err != nil {
|
||||
return definitions.GettableUserConfig{}, fmt.Errorf("failed to unmarshal alertmanager configuration: %w", err)
|
||||
}
|
||||
|
||||
result := definitions.GettableUserConfig{
|
||||
TemplateFiles: cfg.TemplateFiles,
|
||||
AlertmanagerConfig: definitions.GettableApiAlertingConfig{
|
||||
Config: cfg.AlertmanagerConfig.Config,
|
||||
},
|
||||
}
|
||||
|
||||
for _, recv := range cfg.AlertmanagerConfig.Receivers {
|
||||
receivers := make([]*definitions.GettableGrafanaReceiver, 0, len(recv.PostableGrafanaReceivers.GrafanaManagedReceivers))
|
||||
for _, pr := range recv.PostableGrafanaReceivers.GrafanaManagedReceivers {
|
||||
@ -82,7 +117,7 @@ func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Contex
|
||||
result.AlertmanagerConfig.Receivers = append(result.AlertmanagerConfig.Receivers, &gettableApiReceiver)
|
||||
}
|
||||
|
||||
result, err = moa.mergeProvenance(ctx, result, org)
|
||||
result, err = moa.mergeProvenance(ctx, result, orgID)
|
||||
if err != nil {
|
||||
return definitions.GettableUserConfig{}, err
|
||||
}
|
||||
|
@ -63,8 +63,8 @@ grafana_alerting_discovered_configurations 3
|
||||
|
||||
// Configurations should be marked as successfully applied.
|
||||
for _, org := range orgStore.orgs {
|
||||
configs, ok := configStore.appliedConfigs[org]
|
||||
require.True(t, ok)
|
||||
configs, err := configStore.GetAppliedConfigurations(ctx, org, 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, configs, 1)
|
||||
}
|
||||
}
|
||||
@ -184,8 +184,9 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgsWithFailures(t *testing.T)
|
||||
// No successfully applied configurations should be found at first.
|
||||
{
|
||||
for _, org := range orgs {
|
||||
_, ok := configStore.appliedConfigs[org]
|
||||
require.False(t, ok)
|
||||
configs, err := configStore.GetAppliedConfigurations(ctx, org, 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, configs, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,11 +200,11 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgsWithFailures(t *testing.T)
|
||||
|
||||
// Configurations should be marked as successfully applied for all orgs except for org 2.
|
||||
for _, org := range orgs {
|
||||
configs, ok := configStore.appliedConfigs[org]
|
||||
configs, err := configStore.GetAppliedConfigurations(ctx, org, 10)
|
||||
require.NoError(t, err)
|
||||
if org == orgWithBadConfig {
|
||||
require.False(t, ok)
|
||||
require.Len(t, configs, 0)
|
||||
} else {
|
||||
require.True(t, ok)
|
||||
require.Len(t, configs, 1)
|
||||
}
|
||||
}
|
||||
@ -219,11 +220,11 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgsWithFailures(t *testing.T)
|
||||
|
||||
// The configuration should still be marked as successfully applied for all orgs except for org 2.
|
||||
for _, org := range orgs {
|
||||
configs, ok := configStore.appliedConfigs[org]
|
||||
configs, err := configStore.GetAppliedConfigurations(ctx, org, 10)
|
||||
require.NoError(t, err)
|
||||
if org == orgWithBadConfig {
|
||||
require.False(t, ok)
|
||||
require.Len(t, configs, 0)
|
||||
} else {
|
||||
require.True(t, ok)
|
||||
require.Len(t, configs, 1)
|
||||
}
|
||||
}
|
||||
@ -240,9 +241,9 @@ func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgsWithFailures(t *testing.T)
|
||||
|
||||
// All configurations should be marked as successfully applied.
|
||||
for _, org := range orgs {
|
||||
configs, ok := configStore.appliedConfigs[org]
|
||||
require.True(t, ok)
|
||||
require.Len(t, configs, 1)
|
||||
configs, err := configStore.GetAppliedConfigurations(ctx, org, 10)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, len(configs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
@ -17,8 +18,8 @@ import (
|
||||
type fakeConfigStore struct {
|
||||
configs map[int64]*models.AlertConfiguration
|
||||
|
||||
// appliedConfigs stores configs by orgID and config hash.
|
||||
appliedConfigs map[int64]map[string]*models.AlertConfiguration
|
||||
// historicConfigs stores configs by orgID.
|
||||
historicConfigs map[int64][]*models.HistoricAlertConfiguration
|
||||
}
|
||||
|
||||
// Saves the image or returns an error.
|
||||
@ -37,9 +38,15 @@ func (f *fakeConfigStore) GetImages(ctx context.Context, tokens []string) ([]mod
|
||||
func NewFakeConfigStore(t *testing.T, configs map[int64]*models.AlertConfiguration) *fakeConfigStore {
|
||||
t.Helper()
|
||||
|
||||
historicConfigs := make(map[int64][]*models.HistoricAlertConfiguration)
|
||||
for org, config := range configs {
|
||||
historicConfig := models.HistoricConfigFromAlertConfig(*config)
|
||||
historicConfigs[org] = append(historicConfigs[org], &historicConfig)
|
||||
}
|
||||
|
||||
return &fakeConfigStore{
|
||||
configs: configs,
|
||||
appliedConfigs: make(map[int64]map[string]*models.AlertConfiguration),
|
||||
configs: configs,
|
||||
historicConfigs: historicConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,15 +80,14 @@ func (f *fakeConfigStore) SaveAlertmanagerConfigurationWithCallback(_ context.Co
|
||||
}
|
||||
f.configs[cmd.OrgID] = &cfg
|
||||
|
||||
if err := callback(); err != nil {
|
||||
return err
|
||||
historicConfig := models.HistoricConfigFromAlertConfig(cfg)
|
||||
if cmd.LastApplied != 0 {
|
||||
historicConfig.LastApplied = time.Now().UTC().Unix()
|
||||
f.historicConfigs[cmd.OrgID] = append(f.historicConfigs[cmd.OrgID], &historicConfig)
|
||||
}
|
||||
|
||||
if cmd.LastApplied != 0 {
|
||||
if _, ok := f.appliedConfigs[cmd.OrgID]; !ok {
|
||||
f.appliedConfigs[cmd.OrgID] = make(map[string]*models.AlertConfiguration)
|
||||
}
|
||||
f.appliedConfigs[cmd.OrgID][cfg.ConfigurationHash] = &cfg
|
||||
if err := callback(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -89,30 +95,63 @@ func (f *fakeConfigStore) SaveAlertmanagerConfigurationWithCallback(_ context.Co
|
||||
|
||||
func (f *fakeConfigStore) UpdateAlertmanagerConfiguration(_ context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error {
|
||||
if config, exists := f.configs[cmd.OrgID]; exists && config.ConfigurationHash == cmd.FetchedConfigurationHash {
|
||||
f.configs[cmd.OrgID] = &models.AlertConfiguration{
|
||||
newConfig := models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
||||
OrgID: cmd.OrgID,
|
||||
ConfigurationHash: fmt.Sprintf("%x", md5.Sum([]byte(cmd.AlertmanagerConfiguration))),
|
||||
ConfigurationVersion: "v1",
|
||||
Default: cmd.Default,
|
||||
}
|
||||
f.configs[cmd.OrgID] = &newConfig
|
||||
|
||||
historicConfig := models.HistoricConfigFromAlertConfig(newConfig)
|
||||
f.historicConfigs[cmd.OrgID] = append(f.historicConfigs[cmd.OrgID], &historicConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("config not found or hash not valid")
|
||||
}
|
||||
|
||||
func (f *fakeConfigStore) MarkConfigurationAsApplied(_ context.Context, cmd *models.MarkConfigurationAsAppliedCmd) error {
|
||||
for _, config := range f.configs {
|
||||
if config.ConfigurationHash == cmd.ConfigurationHash && config.OrgID == cmd.OrgID {
|
||||
if _, ok := f.appliedConfigs[cmd.OrgID]; !ok {
|
||||
f.appliedConfigs[cmd.OrgID] = make(map[string]*models.AlertConfiguration)
|
||||
orgConfigs, ok := f.historicConfigs[cmd.OrgID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Iterate backwards to find the latest config first.
|
||||
for i := len(orgConfigs) - 1; i >= 0; i-- {
|
||||
for _, config := range orgConfigs {
|
||||
if config.ConfigurationHash == cmd.ConfigurationHash {
|
||||
config.LastApplied = time.Now().UTC().Unix()
|
||||
return nil
|
||||
}
|
||||
f.appliedConfigs[cmd.OrgID][cmd.ConfigurationHash] = config
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("config not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeConfigStore) GetAppliedConfigurations(_ context.Context, orgID int64, limit int) ([]*models.HistoricAlertConfiguration, error) {
|
||||
configsByOrg, ok := f.historicConfigs[orgID]
|
||||
if !ok {
|
||||
return []*models.HistoricAlertConfiguration{}, nil
|
||||
}
|
||||
|
||||
// Iterate backwards to get the latest applied configs.
|
||||
var configs []*models.HistoricAlertConfiguration
|
||||
start := len(configsByOrg) - 1
|
||||
end := start - limit
|
||||
if end < 0 {
|
||||
end = 0
|
||||
}
|
||||
|
||||
for i := start; i >= end; i-- {
|
||||
if configsByOrg[i].LastApplied > 0 {
|
||||
configs = append(configs, configsByOrg[i])
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
type FakeOrgStore struct {
|
||||
|
@ -163,22 +163,30 @@ func (st *DBstore) MarkConfigurationAsApplied(ctx context.Context, cmd *models.M
|
||||
}
|
||||
|
||||
// GetAppliedConfigurations returns all configurations that have been marked as applied, ordered newest -> oldest by id.
|
||||
func (st *DBstore) GetAppliedConfigurations(ctx context.Context, query *models.GetAppliedConfigurationsQuery) (result []*models.HistoricAlertConfiguration, err error) {
|
||||
err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
func (st *DBstore) GetAppliedConfigurations(ctx context.Context, orgID int64, limit int) ([]*models.HistoricAlertConfiguration, error) {
|
||||
if limit < 1 || limit > ConfigRecordsLimit {
|
||||
limit = ConfigRecordsLimit
|
||||
}
|
||||
|
||||
var configs []*models.HistoricAlertConfiguration
|
||||
if err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
cfgs := []*models.HistoricAlertConfiguration{}
|
||||
err := sess.Table("alert_configuration_history").
|
||||
Desc("id").
|
||||
Where("org_id = ? AND last_applied != 0", query.OrgID).
|
||||
Where("org_id = ? AND last_applied != 0", orgID).
|
||||
Limit(limit).
|
||||
Find(&cfgs)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = cfgs
|
||||
configs = cfgs
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}); err != nil {
|
||||
return []*models.HistoricAlertConfiguration{}, err
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (st *DBstore) deleteOldConfigurations(ctx context.Context, orgID int64, limit int) (int64, error) {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@ -296,6 +297,7 @@ func TestIntegrationMarkConfigurationAsApplied(t *testing.T) {
|
||||
|
||||
t.Run("marking an existent config should succeed", func(tt *testing.T) {
|
||||
const orgID = 1
|
||||
limit := 10
|
||||
ctx := context.Background()
|
||||
|
||||
config, _ := setupConfig(t, "test", store)
|
||||
@ -308,10 +310,7 @@ func TestIntegrationMarkConfigurationAsApplied(t *testing.T) {
|
||||
require.NoError(tt, err)
|
||||
|
||||
// Config should be saved but not marked as applied yet.
|
||||
appliedCfgsQuery := models.GetAppliedConfigurationsQuery{
|
||||
OrgID: orgID,
|
||||
}
|
||||
configs, err := store.GetAppliedConfigurations(ctx, &appliedCfgsQuery)
|
||||
configs, err := store.GetAppliedConfigurations(ctx, orgID, limit)
|
||||
require.NoError(tt, err)
|
||||
require.Len(tt, configs, 0)
|
||||
|
||||
@ -329,15 +328,103 @@ func TestIntegrationMarkConfigurationAsApplied(t *testing.T) {
|
||||
require.NoError(tt, err)
|
||||
|
||||
// Config should now be saved and marked as successfully applied.
|
||||
appliedCfgsQuery = models.GetAppliedConfigurationsQuery{
|
||||
OrgID: orgID,
|
||||
}
|
||||
configs, err = store.GetAppliedConfigurations(ctx, &appliedCfgsQuery)
|
||||
configs, err = store.GetAppliedConfigurations(ctx, orgID, limit)
|
||||
require.NoError(tt, err)
|
||||
require.Len(tt, configs, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationGetAppliedConfigurations(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
sqlStore := db.InitTestDB(t)
|
||||
store := &DBstore{
|
||||
SQLStore: sqlStore,
|
||||
Logger: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
t.Run("no configurations = empty slice", func(tt *testing.T) {
|
||||
configs, err := store.GetAppliedConfigurations(context.Background(), 10, 10)
|
||||
require.NoError(tt, err)
|
||||
require.NotNil(tt, configs)
|
||||
require.Len(tt, configs, 0)
|
||||
})
|
||||
|
||||
t.Run("saved configurations marked as applied should be returned", func(tt *testing.T) {
|
||||
ctx := context.Background()
|
||||
var org int64 = 1
|
||||
limit := 10
|
||||
unmarkedConfig, _ := setupConfig(t, "unmarked", store)
|
||||
|
||||
// Save four configurations for the same org.
|
||||
for i := 0; i < 4; i++ {
|
||||
config, _ := setupConfig(t, fmt.Sprintf("test-%d", i+1), store)
|
||||
cmd := &models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: config,
|
||||
ConfigurationVersion: "v1",
|
||||
Default: false,
|
||||
OrgID: org,
|
||||
LastApplied: time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
// Don't mark the third config, that way we have 2 marked, 1 unmarked, 1 marked.
|
||||
if i == 2 {
|
||||
cmd.LastApplied = 0
|
||||
cmd.AlertmanagerConfiguration = unmarkedConfig
|
||||
}
|
||||
|
||||
err := store.SaveAlertmanagerConfiguration(ctx, cmd)
|
||||
require.NoError(tt, err)
|
||||
}
|
||||
|
||||
// Save some configs for other orgs.
|
||||
for i := 0; i < 4; i++ {
|
||||
config, _ := setupConfig(t, fmt.Sprintf("test-%d", i+1), store)
|
||||
cmd := &models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: config,
|
||||
ConfigurationVersion: "v1",
|
||||
Default: false,
|
||||
OrgID: int64(i) + org + 1, // This way we avoid saving more configs for the same org.
|
||||
LastApplied: time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
err := store.SaveAlertmanagerConfiguration(ctx, cmd)
|
||||
require.NoError(tt, err)
|
||||
}
|
||||
|
||||
configs, err := store.GetAppliedConfigurations(ctx, org, limit)
|
||||
require.NoError(tt, err)
|
||||
require.Len(tt, configs, 3)
|
||||
|
||||
var lastID int64
|
||||
for _, config := range configs {
|
||||
// Check that the returned configurations are the ones that we're expecting.
|
||||
require.NotEqual(tt, config.AlertConfiguration.AlertmanagerConfiguration, unmarkedConfig)
|
||||
|
||||
// Configs should only belong to the queried org.
|
||||
require.Equal(tt, org, config.OrgID)
|
||||
|
||||
// LastApplied must not be zero.
|
||||
require.NotZero(tt, config.LastApplied)
|
||||
|
||||
// Configs should be returned in descending order (id).
|
||||
if lastID != 0 {
|
||||
require.LessOrEqual(tt, config.AlertConfiguration.ID, lastID)
|
||||
}
|
||||
lastID = config.AlertConfiguration.ID
|
||||
}
|
||||
|
||||
// The limit should be considered by the store.
|
||||
// The only record returned should be the latest one (highest id).
|
||||
highestID := configs[0].ID
|
||||
configs, err = store.GetAppliedConfigurations(ctx, org, 1)
|
||||
require.NoError(tt, err)
|
||||
require.Len(tt, configs, 1)
|
||||
require.Equal(tt, configs[0].ID, highestID)
|
||||
})
|
||||
}
|
||||
|
||||
func setupConfig(t *testing.T, config string, store *DBstore) (string, string) {
|
||||
t.Helper()
|
||||
return setupConfigInOrg(t, config, 1, store)
|
||||
|
@ -28,6 +28,7 @@ type AlertingStore interface {
|
||||
SaveAlertmanagerConfigurationWithCallback(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd, callback SaveCallback) error
|
||||
UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error
|
||||
MarkConfigurationAsApplied(ctx context.Context, cmd *models.MarkConfigurationAsAppliedCmd) error
|
||||
GetAppliedConfigurations(ctx context.Context, orgID int64, limit int) ([]*models.HistoricAlertConfiguration, error)
|
||||
}
|
||||
|
||||
// DBstore stores the alert definitions and instances in the database.
|
||||
|
Reference in New Issue
Block a user