Alerting: Guided legacy alerting upgrade dry-run (#80071)

This PR has two steps that together create a functional dry-run capability for the migration.

By enabling the feature flag alertingPreviewUpgrade when on legacy alerting it will:
    a. Allow all Grafana Alerting background services except for the scheduler to start (multiorg alertmanager, state manager, routes, …).
    b. Allow the UI to show Grafana Alerting pages alongside legacy ones (with appropriate in-app warnings that UA is not actually running).
    c. Show a new “Alerting Upgrade” page and register associated /api/v1/upgrade endpoints that will allow the user to upgrade their organization live without restart and present a summary of the upgrade in a table.
This commit is contained in:
Matthew Jacobson
2024-01-05 18:19:12 -05:00
committed by GitHub
parent 72182e02a4
commit aa03b8f8a7
42 changed files with 7403 additions and 169 deletions

View File

@@ -168,10 +168,17 @@ func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmode
func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *navtree.NavTreeRoot, plugin pluginstore.Plugin, appLink *navtree.NavLink) {
// Handle moving apps into specific navtree sections
var alertingNodes []*navtree.NavLink
alertingNode := treeRoot.FindById(navtree.NavIDAlerting)
if alertingNode == nil {
// Search for legacy alerting node just in case
alertingNode = treeRoot.FindById(navtree.NavIDAlertingLegacy)
if alertingNode != nil {
alertingNodes = append(alertingNodes, alertingNode)
}
if len(alertingNodes) == 0 || s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingPreviewUpgrade) {
// If FlagAlertingPreviewUpgrade is enabled, we want to add both the legacy and new alerting nodes.
alertingNode := treeRoot.FindById(navtree.NavIDAlertingLegacy)
if alertingNode != nil {
alertingNodes = append(alertingNodes, alertingNode)
}
}
sectionID := navtree.NavIDApps
@@ -235,7 +242,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n
})
case navtree.NavIDAlertsAndIncidents:
alertsAndIncidentsChildren := []*navtree.NavLink{}
if alertingNode != nil {
for _, alertingNode := range alertingNodes {
alertsAndIncidentsChildren = append(alertsAndIncidentsChildren, alertingNode)
treeRoot.RemoveSection(alertingNode)
}

View File

@@ -138,6 +138,11 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
if legacyAlertSection := s.buildLegacyAlertNavLinks(c); legacyAlertSection != nil {
treeRoot.AddSection(legacyAlertSection)
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingPreviewUpgrade) && !uaIsDisabledForOrg {
if alertingSection := s.buildAlertNavLinks(c); alertingSection != nil {
treeRoot.AddSection(alertingSection)
}
}
} else if uaVisibleForOrg {
if alertingSection := s.buildAlertNavLinks(c); alertingSection != nil {
treeRoot.AddSection(alertingSection)
@@ -404,6 +409,14 @@ func (s *ServiceImpl) buildLegacyAlertNavLinks(c *contextmodel.ReqContext) *navt
})
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingPreviewUpgrade) && c.HasRole(org.RoleAdmin) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Upgrade Alerting", Id: "alerting-upgrade", Url: s.cfg.AppSubURL + "/alerting-legacy/upgrade",
SubTitle: "Upgrade your existing legacy alerts and notification channels to the new Grafana Alerting",
Icon: "cog",
})
}
var alertNav = navtree.NavLink{
Text: "Alerting",
SubTitle: "Learn about problems in your systems moments after they occur",

View File

@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/backtesting"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/migration"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
@@ -73,6 +74,7 @@ type API struct {
Historian Historian
Tracer tracing.Tracer
AppUrl *url.URL
UpgradeService migration.UpgradeService
// Hooks can be used to replace API handlers for specific paths.
Hooks *Hooks
@@ -149,4 +151,13 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
logger: logger,
hist: api.Historian,
}), m)
// Inject upgrade endpoints if legacy alerting is enabled and the feature flag is enabled.
if !api.Cfg.UnifiedAlerting.IsEnabled() && api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingPreviewUpgrade) {
api.RegisterUpgradeApiEndpoints(NewUpgradeApi(NewUpgradeSrc(
logger,
api.UpgradeService,
api.Cfg,
)), m)
}
}

View File

@@ -0,0 +1,140 @@
package api
import (
"errors"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ngalert/migration"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type UpgradeSrv struct {
log log.Logger
upgradeService migration.UpgradeService
cfg *setting.Cfg
}
func NewUpgradeSrc(
log log.Logger,
upgradeService migration.UpgradeService,
cfg *setting.Cfg,
) *UpgradeSrv {
return &UpgradeSrv{
log: log,
upgradeService: upgradeService,
cfg: cfg,
}
}
func (srv *UpgradeSrv) RoutePostUpgradeOrg(c *contextmodel.ReqContext) response.Response {
summary, err := srv.upgradeService.MigrateOrg(c.Req.Context(), c.OrgID, c.QueryBool("skipExisting"))
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, summary)
}
func (srv *UpgradeSrv) RouteGetOrgUpgrade(c *contextmodel.ReqContext) response.Response {
state, err := srv.upgradeService.GetOrgMigrationState(c.Req.Context(), c.OrgID)
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, state)
}
func (srv *UpgradeSrv) RouteDeleteOrgUpgrade(c *contextmodel.ReqContext) response.Response {
err := srv.upgradeService.RevertOrg(c.Req.Context(), c.OrgID)
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, util.DynMap{"message": "Grafana Alerting resources deleted for this organization."})
}
func (srv *UpgradeSrv) RoutePostUpgradeAlert(c *contextmodel.ReqContext, dashboardIdParam string, panelIdParam string) response.Response {
dashboardId, err := strconv.ParseInt(dashboardIdParam, 10, 64)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "failed to parse dashboardId")
}
panelId, err := strconv.ParseInt(panelIdParam, 10, 64)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "failed to parse panelId")
}
summary, err := srv.upgradeService.MigrateAlert(c.Req.Context(), c.OrgID, dashboardId, panelId)
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, summary)
}
func (srv *UpgradeSrv) RoutePostUpgradeDashboard(c *contextmodel.ReqContext, dashboardIdParam string) response.Response {
dashboardId, err := strconv.ParseInt(dashboardIdParam, 10, 64)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "failed to parse dashboardId")
}
summary, err := srv.upgradeService.MigrateDashboardAlerts(c.Req.Context(), c.OrgID, dashboardId, c.QueryBool("skipExisting"))
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, summary)
}
func (srv *UpgradeSrv) RoutePostUpgradeAllDashboards(c *contextmodel.ReqContext) response.Response {
summary, err := srv.upgradeService.MigrateAllDashboardAlerts(c.Req.Context(), c.OrgID, c.QueryBool("skipExisting"))
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, summary)
}
func (srv *UpgradeSrv) RoutePostUpgradeChannel(c *contextmodel.ReqContext, channelIdParam string) response.Response {
channelId, err := strconv.ParseInt(channelIdParam, 10, 64)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "failed to parse channelId")
}
summary, err := srv.upgradeService.MigrateChannel(c.Req.Context(), c.OrgID, channelId)
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, summary)
}
func (srv *UpgradeSrv) RoutePostUpgradeAllChannels(c *contextmodel.ReqContext) response.Response {
summary, err := srv.upgradeService.MigrateAllChannels(c.Req.Context(), c.OrgID, c.QueryBool("skipExisting"))
if err != nil {
if errors.Is(err, migration.ErrUpgradeInProgress) {
return response.Error(http.StatusConflict, "Upgrade already in progress", err)
}
return response.Error(http.StatusInternalServerError, "Server error", err)
}
return response.JSON(http.StatusOK, summary)
}

View File

@@ -43,10 +43,28 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingRuleCreate, scope),
ac.EvalPermission(ac.ActionAlertingRuleDelete, scope),
)
// Grafana rule state history paths
// Grafana rule state history paths
case http.MethodGet + "/api/v1/rules/history":
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
// Grafana unified alerting upgrade paths
case http.MethodGet + "/api/v1/upgrade/org":
return middleware.ReqOrgAdmin
case http.MethodPost + "/api/v1/upgrade/org":
return middleware.ReqOrgAdmin
case http.MethodDelete + "/api/v1/upgrade/org":
return middleware.ReqOrgAdmin
case http.MethodPost + "/api/v1/upgrade/dashboards":
return middleware.ReqOrgAdmin
case http.MethodPost + "/api/v1/upgrade/dashboards/{DashboardID}":
return middleware.ReqOrgAdmin
case http.MethodPost + "/api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID}":
return middleware.ReqOrgAdmin
case http.MethodPost + "/api/v1/upgrade/channels":
return middleware.ReqOrgAdmin
case http.MethodPost + "/api/v1/upgrade/channels/{ChannelID}":
return middleware.ReqOrgAdmin
// Grafana, Prometheus-compatible Paths
case http.MethodGet + "/api/prometheus/grafana/api/v1/rules":
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)

View File

@@ -40,7 +40,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 54)
require.Len(t, paths, 60)
ac := acmock.New()
api := &API{AccessControl: ac}

View File

@@ -0,0 +1,163 @@
/*Package api contains base API implementation of unified alerting
*
*Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
*
*Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them.
*/
package api
import (
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/web"
)
type UpgradeApi interface {
RouteDeleteOrgUpgrade(*contextmodel.ReqContext) response.Response
RouteGetOrgUpgrade(*contextmodel.ReqContext) response.Response
RoutePostUpgradeAlert(*contextmodel.ReqContext) response.Response
RoutePostUpgradeAllChannels(*contextmodel.ReqContext) response.Response
RoutePostUpgradeAllDashboards(*contextmodel.ReqContext) response.Response
RoutePostUpgradeChannel(*contextmodel.ReqContext) response.Response
RoutePostUpgradeDashboard(*contextmodel.ReqContext) response.Response
RoutePostUpgradeOrg(*contextmodel.ReqContext) response.Response
}
func (f *UpgradeApiHandler) RouteDeleteOrgUpgrade(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteDeleteOrgUpgrade(ctx)
}
func (f *UpgradeApiHandler) RouteGetOrgUpgrade(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteGetOrgUpgrade(ctx)
}
func (f *UpgradeApiHandler) RoutePostUpgradeAlert(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
dashboardIDParam := web.Params(ctx.Req)[":DashboardID"]
panelIDParam := web.Params(ctx.Req)[":PanelID"]
return f.handleRoutePostUpgradeAlert(ctx, dashboardIDParam, panelIDParam)
}
func (f *UpgradeApiHandler) RoutePostUpgradeAllChannels(ctx *contextmodel.ReqContext) response.Response {
return f.handleRoutePostUpgradeAllChannels(ctx)
}
func (f *UpgradeApiHandler) RoutePostUpgradeAllDashboards(ctx *contextmodel.ReqContext) response.Response {
return f.handleRoutePostUpgradeAllDashboards(ctx)
}
func (f *UpgradeApiHandler) RoutePostUpgradeChannel(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
channelIDParam := web.Params(ctx.Req)[":ChannelID"]
return f.handleRoutePostUpgradeChannel(ctx, channelIDParam)
}
func (f *UpgradeApiHandler) RoutePostUpgradeDashboard(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
dashboardIDParam := web.Params(ctx.Req)[":DashboardID"]
return f.handleRoutePostUpgradeDashboard(ctx, dashboardIDParam)
}
func (f *UpgradeApiHandler) RoutePostUpgradeOrg(ctx *contextmodel.ReqContext) response.Response {
return f.handleRoutePostUpgradeOrg(ctx)
}
func (api *API) RegisterUpgradeApiEndpoints(srv UpgradeApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Delete(
toMacaronPath("/api/v1/upgrade/org"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodDelete, "/api/v1/upgrade/org"),
metrics.Instrument(
http.MethodDelete,
"/api/v1/upgrade/org",
api.Hooks.Wrap(srv.RouteDeleteOrgUpgrade),
m,
),
)
group.Get(
toMacaronPath("/api/v1/upgrade/org"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/v1/upgrade/org"),
metrics.Instrument(
http.MethodGet,
"/api/v1/upgrade/org",
api.Hooks.Wrap(srv.RouteGetOrgUpgrade),
m,
),
)
group.Post(
toMacaronPath("/api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID}"),
metrics.Instrument(
http.MethodPost,
"/api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID}",
api.Hooks.Wrap(srv.RoutePostUpgradeAlert),
m,
),
)
group.Post(
toMacaronPath("/api/v1/upgrade/channels"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/v1/upgrade/channels"),
metrics.Instrument(
http.MethodPost,
"/api/v1/upgrade/channels",
api.Hooks.Wrap(srv.RoutePostUpgradeAllChannels),
m,
),
)
group.Post(
toMacaronPath("/api/v1/upgrade/dashboards"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/v1/upgrade/dashboards"),
metrics.Instrument(
http.MethodPost,
"/api/v1/upgrade/dashboards",
api.Hooks.Wrap(srv.RoutePostUpgradeAllDashboards),
m,
),
)
group.Post(
toMacaronPath("/api/v1/upgrade/channels/{ChannelID}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/v1/upgrade/channels/{ChannelID}"),
metrics.Instrument(
http.MethodPost,
"/api/v1/upgrade/channels/{ChannelID}",
api.Hooks.Wrap(srv.RoutePostUpgradeChannel),
m,
),
)
group.Post(
toMacaronPath("/api/v1/upgrade/dashboards/{DashboardID}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/v1/upgrade/dashboards/{DashboardID}"),
metrics.Instrument(
http.MethodPost,
"/api/v1/upgrade/dashboards/{DashboardID}",
api.Hooks.Wrap(srv.RoutePostUpgradeDashboard),
m,
),
)
group.Post(
toMacaronPath("/api/v1/upgrade/org"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/v1/upgrade/org"),
metrics.Instrument(
http.MethodPost,
"/api/v1/upgrade/org",
api.Hooks.Wrap(srv.RoutePostUpgradeOrg),
m,
),
)
}, middleware.ReqSignedIn)
}

View File

@@ -96,6 +96,20 @@
"title": "AlertManagersResult contains the result from querying the alertmanagers endpoint.",
"type": "object"
},
"AlertPair": {
"properties": {
"alertRule": {
"$ref": "#/definitions/AlertRuleUpgrade"
},
"error": {
"type": "string"
},
"legacyAlert": {
"$ref": "#/definitions/LegacyAlert"
}
},
"type": "object"
},
"AlertQuery": {
"properties": {
"datasourceUid": {
@@ -280,6 +294,23 @@
},
"type": "object"
},
"AlertRuleUpgrade": {
"properties": {
"sendsTo": {
"items": {
"type": "string"
},
"type": "array"
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"type": "object"
},
"AlertingFileExport": {
"properties": {
"apiVersion": {
@@ -561,6 +592,20 @@
"title": "Config is the top-level configuration for Alertmanager's config files.",
"type": "object"
},
"ContactPair": {
"properties": {
"contactPoint": {
"$ref": "#/definitions/ContactPointUpgrade"
},
"error": {
"type": "string"
},
"legacyChannel": {
"$ref": "#/definitions/LegacyChannel"
}
},
"type": "object"
},
"ContactPointExport": {
"properties": {
"name": {
@@ -580,6 +625,20 @@
"title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.",
"type": "object"
},
"ContactPointUpgrade": {
"properties": {
"name": {
"type": "string"
},
"routeMatchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"ContactPoints": {
"items": {
"$ref": "#/definitions/EmbeddedContactPoint"
@@ -592,6 +651,48 @@
"title": "CounterResetHint contains the known information about a counter reset,",
"type": "integer"
},
"DashboardUpgrade": {
"properties": {
"dashboardId": {
"format": "int64",
"type": "integer"
},
"dashboardName": {
"type": "string"
},
"dashboardUid": {
"type": "string"
},
"error": {
"type": "string"
},
"folderName": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"migratedAlerts": {
"items": {
"$ref": "#/definitions/AlertPair"
},
"type": "array"
},
"newFolderName": {
"type": "string"
},
"newFolderUid": {
"type": "string"
},
"provisioned": {
"type": "boolean"
},
"warning": {
"type": "string"
}
},
"type": "object"
},
"DataLink": {
"description": "DataLink define what",
"properties": {
@@ -1833,6 +1934,41 @@
},
"type": "array"
},
"LegacyAlert": {
"properties": {
"dashboardId": {
"format": "int64",
"type": "integer"
},
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"panelId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"LegacyChannel": {
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"LinkTransformationConfig": {
"properties": {
"expression": {
@@ -2209,6 +2345,50 @@
},
"type": "object"
},
"OrgMigrationState": {
"properties": {
"migratedChannels": {
"items": {
"$ref": "#/definitions/ContactPair"
},
"type": "array"
},
"migratedDashboards": {
"items": {
"$ref": "#/definitions/DashboardUpgrade"
},
"type": "array"
},
"orgId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"OrgMigrationSummary": {
"properties": {
"hasErrors": {
"type": "boolean"
},
"newAlerts": {
"format": "int64",
"type": "integer"
},
"newChannels": {
"format": "int64",
"type": "integer"
},
"newDashboards": {
"format": "int64",
"type": "integer"
},
"removed": {
"type": "boolean"
}
},
"type": "object"
},
"PagerdutyConfig": {
"properties": {
"class": {
@@ -4248,6 +4428,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup"
},
@@ -4468,6 +4649,7 @@
"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",
@@ -4611,7 +4793,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@@ -4649,7 +4830,6 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",

View File

@@ -0,0 +1,189 @@
package definitions
// swagger:route GET /api/v1/upgrade/org upgrade RouteGetOrgUpgrade
//
// Get existing alerting upgrade for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationState
// swagger:route POST /api/v1/upgrade/org upgrade RoutePostUpgradeOrg
//
// Upgrade all legacy alerts for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationSummary
// swagger:route DELETE /api/v1/upgrade/org upgrade RouteDeleteOrgUpgrade
//
// Delete existing alerting upgrade for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: Ack
// swagger:route POST /api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID} upgrade RoutePostUpgradeAlert
//
// Upgrade single legacy dashboard alert for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationSummary
// swagger:route POST /api/v1/upgrade/dashboards/{DashboardID} upgrade RoutePostUpgradeDashboard
//
// Upgrade all legacy dashboard alerts on a dashboard for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationSummary
// swagger:route POST /api/v1/upgrade/dashboards upgrade RoutePostUpgradeAllDashboards
//
// Upgrade all legacy dashboard alerts for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationSummary
// swagger:route POST /api/v1/upgrade/channels upgrade RoutePostUpgradeAllChannels
//
// Upgrade all legacy notification channels for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationSummary
// swagger:route POST /api/v1/upgrade/channels/{ChannelID} upgrade RoutePostUpgradeChannel
//
// Upgrade a single legacy notification channel for the current organization.
//
// Produces:
// - application/json
//
// Responses:
// 200: OrgMigrationSummary
// swagger:parameters RoutePostUpgradeOrg RoutePostUpgradeDashboard RoutePostUpgradeAllChannels
type SkipExistingQueryParam struct {
// If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.
// in:query
// required:false
// default:false
SkipExisting bool
}
// swagger:parameters RoutePostUpgradeAlert RoutePostUpgradeDashboard
type DashboardParam struct {
// Dashboard ID of dashboard alert.
// in:path
// required:true
DashboardID string
}
// swagger:parameters RoutePostUpgradeAlert
type PanelParam struct {
// Panel ID of dashboard alert.
// in:path
// required:true
PanelID string
}
// swagger:parameters RoutePostUpgradeChannel
type ChannelParam struct {
// Channel ID of legacy notification channel.
// in:path
// required:true
ChannelID string
}
// swagger:model
type OrgMigrationSummary struct {
NewDashboards int `json:"newDashboards"`
NewAlerts int `json:"newAlerts"`
NewChannels int `json:"newChannels"`
Removed bool `json:"removed"`
HasErrors bool `json:"hasErrors"`
}
func (s *OrgMigrationSummary) Add(other OrgMigrationSummary) {
s.NewDashboards += other.NewDashboards
s.NewAlerts += other.NewAlerts
s.NewChannels += other.NewChannels
s.Removed = s.Removed || other.Removed
s.HasErrors = s.HasErrors || other.HasErrors
}
// swagger:model
type OrgMigrationState struct {
OrgID int64 `json:"orgId"`
MigratedDashboards []*DashboardUpgrade `json:"migratedDashboards"`
MigratedChannels []*ContactPair `json:"migratedChannels"`
}
type DashboardUpgrade struct {
MigratedAlerts []*AlertPair `json:"migratedAlerts"`
DashboardID int64 `json:"dashboardId"`
DashboardUID string `json:"dashboardUid"`
DashboardName string `json:"dashboardName"`
FolderUID string `json:"folderUid"`
FolderName string `json:"folderName"`
NewFolderUID string `json:"newFolderUid,omitempty"`
NewFolderName string `json:"newFolderName,omitempty"`
Provisioned bool `json:"provisioned"`
Warning string `json:"warning"`
Error string `json:"error,omitempty"`
}
type AlertPair struct {
LegacyAlert *LegacyAlert `json:"legacyAlert"`
AlertRule *AlertRuleUpgrade `json:"alertRule"`
Error string `json:"error,omitempty"`
}
type ContactPair struct {
LegacyChannel *LegacyChannel `json:"legacyChannel"`
ContactPointUpgrade *ContactPointUpgrade `json:"contactPoint"`
Error string `json:"error,omitempty"`
}
type LegacyAlert struct {
ID int64 `json:"id"`
DashboardID int64 `json:"dashboardId"`
PanelID int64 `json:"panelId"`
Name string `json:"name"`
}
type AlertRuleUpgrade struct {
UID string `json:"uid"`
Title string `json:"title"`
SendsTo []string `json:"sendsTo"`
}
type LegacyChannel struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type ContactPointUpgrade struct {
Name string `json:"name"`
Type string `json:"type"`
RouteMatchers ObjectMatchers `json:"routeMatchers"`
}

View File

@@ -96,6 +96,20 @@
"title": "AlertManagersResult contains the result from querying the alertmanagers endpoint.",
"type": "object"
},
"AlertPair": {
"properties": {
"alertRule": {
"$ref": "#/definitions/AlertRuleUpgrade"
},
"error": {
"type": "string"
},
"legacyAlert": {
"$ref": "#/definitions/LegacyAlert"
}
},
"type": "object"
},
"AlertQuery": {
"properties": {
"datasourceUid": {
@@ -280,6 +294,23 @@
},
"type": "object"
},
"AlertRuleUpgrade": {
"properties": {
"sendsTo": {
"items": {
"type": "string"
},
"type": "array"
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"type": "object"
},
"AlertingFileExport": {
"properties": {
"apiVersion": {
@@ -561,6 +592,20 @@
"title": "Config is the top-level configuration for Alertmanager's config files.",
"type": "object"
},
"ContactPair": {
"properties": {
"contactPoint": {
"$ref": "#/definitions/ContactPointUpgrade"
},
"error": {
"type": "string"
},
"legacyChannel": {
"$ref": "#/definitions/LegacyChannel"
}
},
"type": "object"
},
"ContactPointExport": {
"properties": {
"name": {
@@ -580,6 +625,20 @@
"title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.",
"type": "object"
},
"ContactPointUpgrade": {
"properties": {
"name": {
"type": "string"
},
"routeMatchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"ContactPoints": {
"items": {
"$ref": "#/definitions/EmbeddedContactPoint"
@@ -592,6 +651,48 @@
"title": "CounterResetHint contains the known information about a counter reset,",
"type": "integer"
},
"DashboardUpgrade": {
"properties": {
"dashboardId": {
"format": "int64",
"type": "integer"
},
"dashboardName": {
"type": "string"
},
"dashboardUid": {
"type": "string"
},
"error": {
"type": "string"
},
"folderName": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"migratedAlerts": {
"items": {
"$ref": "#/definitions/AlertPair"
},
"type": "array"
},
"newFolderName": {
"type": "string"
},
"newFolderUid": {
"type": "string"
},
"provisioned": {
"type": "boolean"
},
"warning": {
"type": "string"
}
},
"type": "object"
},
"DataLink": {
"description": "DataLink define what",
"properties": {
@@ -1833,6 +1934,41 @@
},
"type": "array"
},
"LegacyAlert": {
"properties": {
"dashboardId": {
"format": "int64",
"type": "integer"
},
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"panelId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"LegacyChannel": {
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"LinkTransformationConfig": {
"properties": {
"expression": {
@@ -2209,6 +2345,50 @@
},
"type": "object"
},
"OrgMigrationState": {
"properties": {
"migratedChannels": {
"items": {
"$ref": "#/definitions/ContactPair"
},
"type": "array"
},
"migratedDashboards": {
"items": {
"$ref": "#/definitions/DashboardUpgrade"
},
"type": "array"
},
"orgId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"OrgMigrationSummary": {
"properties": {
"hasErrors": {
"type": "boolean"
},
"newAlerts": {
"format": "int64",
"type": "integer"
},
"newChannels": {
"format": "int64",
"type": "integer"
},
"newDashboards": {
"format": "int64",
"type": "integer"
},
"removed": {
"type": "boolean"
}
},
"type": "object"
},
"PagerdutyConfig": {
"properties": {
"class": {
@@ -3984,7 +4164,6 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\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"
@@ -4020,7 +4199,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4250,6 +4429,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup"
},
@@ -4354,6 +4534,7 @@
"type": "object"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/definitions/labelSet"
@@ -4464,12 +4645,14 @@
"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",
@@ -4613,7 +4796,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@@ -4651,6 +4833,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",
@@ -7913,6 +8096,221 @@
"history"
]
}
},
"/api/v1/upgrade/channels": {
"post": {
"operationId": "RoutePostUpgradeAllChannels",
"parameters": [
{
"default": false,
"description": "If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.",
"in": "query",
"name": "SkipExisting",
"type": "boolean"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
},
"summary": "Upgrade all legacy notification channels for the current organization.",
"tags": [
"upgrade"
]
}
},
"/api/v1/upgrade/channels/{ChannelID}": {
"post": {
"operationId": "RoutePostUpgradeChannel",
"parameters": [
{
"description": "Channel ID of legacy notification channel.",
"in": "path",
"name": "ChannelID",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
},
"summary": "Upgrade a single legacy notification channel for the current organization.",
"tags": [
"upgrade"
]
}
},
"/api/v1/upgrade/dashboards": {
"post": {
"operationId": "RoutePostUpgradeAllDashboards",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
},
"summary": "Upgrade all legacy dashboard alerts for the current organization.",
"tags": [
"upgrade"
]
}
},
"/api/v1/upgrade/dashboards/{DashboardID}": {
"post": {
"operationId": "RoutePostUpgradeDashboard",
"parameters": [
{
"default": false,
"description": "If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.",
"in": "query",
"name": "SkipExisting",
"type": "boolean"
},
{
"description": "Dashboard ID of dashboard alert.",
"in": "path",
"name": "DashboardID",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
},
"summary": "Upgrade all legacy dashboard alerts on a dashboard for the current organization.",
"tags": [
"upgrade"
]
}
},
"/api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID}": {
"post": {
"operationId": "RoutePostUpgradeAlert",
"parameters": [
{
"description": "Dashboard ID of dashboard alert.",
"in": "path",
"name": "DashboardID",
"required": true,
"type": "string"
},
{
"description": "Panel ID of dashboard alert.",
"in": "path",
"name": "PanelID",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
},
"summary": "Upgrade single legacy dashboard alert for the current organization.",
"tags": [
"upgrade"
]
}
},
"/api/v1/upgrade/org": {
"delete": {
"operationId": "RouteDeleteOrgUpgrade",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
}
},
"summary": "Delete existing alerting upgrade for the current organization.",
"tags": [
"upgrade"
]
},
"get": {
"operationId": "RouteGetOrgUpgrade",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationState",
"schema": {
"$ref": "#/definitions/OrgMigrationState"
}
}
},
"summary": "Get existing alerting upgrade for the current organization.",
"tags": [
"upgrade"
]
},
"post": {
"operationId": "RoutePostUpgradeOrg",
"parameters": [
{
"default": false,
"description": "If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.",
"in": "query",
"name": "SkipExisting",
"type": "boolean"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
},
"summary": "Upgrade all legacy alerts for the current organization.",
"tags": [
"upgrade"
]
}
}
},
"produces": [

View File

@@ -3190,6 +3190,221 @@
}
}
}
},
"/api/v1/upgrade/channels": {
"post": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Upgrade all legacy notification channels for the current organization.",
"operationId": "RoutePostUpgradeAllChannels",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.",
"name": "SkipExisting",
"in": "query"
}
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
}
}
},
"/api/v1/upgrade/channels/{ChannelID}": {
"post": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Upgrade a single legacy notification channel for the current organization.",
"operationId": "RoutePostUpgradeChannel",
"parameters": [
{
"type": "string",
"description": "Channel ID of legacy notification channel.",
"name": "ChannelID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
}
}
},
"/api/v1/upgrade/dashboards": {
"post": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Upgrade all legacy dashboard alerts for the current organization.",
"operationId": "RoutePostUpgradeAllDashboards",
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
}
}
},
"/api/v1/upgrade/dashboards/{DashboardID}": {
"post": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Upgrade all legacy dashboard alerts on a dashboard for the current organization.",
"operationId": "RoutePostUpgradeDashboard",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.",
"name": "SkipExisting",
"in": "query"
},
{
"type": "string",
"description": "Dashboard ID of dashboard alert.",
"name": "DashboardID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
}
}
},
"/api/v1/upgrade/dashboards/{DashboardID}/panels/{PanelID}": {
"post": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Upgrade single legacy dashboard alert for the current organization.",
"operationId": "RoutePostUpgradeAlert",
"parameters": [
{
"type": "string",
"description": "Dashboard ID of dashboard alert.",
"name": "DashboardID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Panel ID of dashboard alert.",
"name": "PanelID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
}
}
},
"/api/v1/upgrade/org": {
"get": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Get existing alerting upgrade for the current organization.",
"operationId": "RouteGetOrgUpgrade",
"responses": {
"200": {
"description": "OrgMigrationState",
"schema": {
"$ref": "#/definitions/OrgMigrationState"
}
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Upgrade all legacy alerts for the current organization.",
"operationId": "RoutePostUpgradeOrg",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "If true, legacy alert and notification channel upgrades from previous runs will be skipped. Otherwise, they will be replaced.",
"name": "SkipExisting",
"in": "query"
}
],
"responses": {
"200": {
"description": "OrgMigrationSummary",
"schema": {
"$ref": "#/definitions/OrgMigrationSummary"
}
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"upgrade"
],
"summary": "Delete existing alerting upgrade for the current organization.",
"operationId": "RouteDeleteOrgUpgrade",
"responses": {
"200": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
}
}
}
}
},
"definitions": {
@@ -3285,6 +3500,20 @@
}
}
},
"AlertPair": {
"type": "object",
"properties": {
"alertRule": {
"$ref": "#/definitions/AlertRuleUpgrade"
},
"error": {
"type": "string"
},
"legacyAlert": {
"$ref": "#/definitions/LegacyAlert"
}
}
},
"AlertQuery": {
"type": "object",
"title": "AlertQuery represents a single query associated with an alert definition.",
@@ -3469,6 +3698,23 @@
}
}
},
"AlertRuleUpgrade": {
"type": "object",
"properties": {
"sendsTo": {
"type": "array",
"items": {
"type": "string"
}
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
"AlertingFileExport": {
"type": "object",
"title": "AlertingFileExport is the full provisioned file export.",
@@ -3750,6 +3996,20 @@
}
}
},
"ContactPair": {
"type": "object",
"properties": {
"contactPoint": {
"$ref": "#/definitions/ContactPointUpgrade"
},
"error": {
"type": "string"
},
"legacyChannel": {
"$ref": "#/definitions/LegacyChannel"
}
}
},
"ContactPointExport": {
"type": "object",
"title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.",
@@ -3769,6 +4029,20 @@
}
}
},
"ContactPointUpgrade": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"routeMatchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"type": {
"type": "string"
}
}
},
"ContactPoints": {
"type": "array",
"items": {
@@ -3781,6 +4055,48 @@
"format": "uint8",
"title": "CounterResetHint contains the known information about a counter reset,"
},
"DashboardUpgrade": {
"type": "object",
"properties": {
"dashboardId": {
"type": "integer",
"format": "int64"
},
"dashboardName": {
"type": "string"
},
"dashboardUid": {
"type": "string"
},
"error": {
"type": "string"
},
"folderName": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"migratedAlerts": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertPair"
}
},
"newFolderName": {
"type": "string"
},
"newFolderUid": {
"type": "string"
},
"provisioned": {
"type": "boolean"
},
"warning": {
"type": "string"
}
}
},
"DataLink": {
"description": "DataLink define what",
"type": "object",
@@ -5025,6 +5341,41 @@
"$ref": "#/definitions/Label"
}
},
"LegacyAlert": {
"type": "object",
"properties": {
"dashboardId": {
"type": "integer",
"format": "int64"
},
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"panelId": {
"type": "integer",
"format": "int64"
}
}
},
"LegacyChannel": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"LinkTransformationConfig": {
"type": "object",
"properties": {
@@ -5402,6 +5753,50 @@
}
}
},
"OrgMigrationState": {
"type": "object",
"properties": {
"migratedChannels": {
"type": "array",
"items": {
"$ref": "#/definitions/ContactPair"
}
},
"migratedDashboards": {
"type": "array",
"items": {
"$ref": "#/definitions/DashboardUpgrade"
}
},
"orgId": {
"type": "integer",
"format": "int64"
}
}
},
"OrgMigrationSummary": {
"type": "object",
"properties": {
"hasErrors": {
"type": "boolean"
},
"newAlerts": {
"type": "integer",
"format": "int64"
},
"newChannels": {
"type": "integer",
"format": "int64"
},
"newDashboards": {
"type": "integer",
"format": "int64"
},
"removed": {
"type": "boolean"
}
}
},
"PagerdutyConfig": {
"type": "object",
"title": "PagerdutyConfig configures notifications via PagerDuty.",
@@ -7177,9 +7572,8 @@
}
},
"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 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).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -7444,6 +7838,7 @@
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"$ref": "#/definitions/alertGroup"
@@ -7549,6 +7944,7 @@
}
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object",
"required": [
"labels",
@@ -7662,6 +8058,7 @@
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
@@ -7669,6 +8066,7 @@
"$ref": "#/definitions/gettableSilences"
},
"integration": {
"description": "Integration integration",
"type": "object",
"required": [
"name",
@@ -7813,7 +8211,6 @@
}
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",
@@ -7852,6 +8249,7 @@
"$ref": "#/definitions/postableSilence"
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"active",

View File

@@ -0,0 +1,48 @@
package api
import (
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
)
type UpgradeApiHandler struct {
svc *UpgradeSrv
}
func NewUpgradeApi(svc *UpgradeSrv) *UpgradeApiHandler {
return &UpgradeApiHandler{
svc: svc,
}
}
func (f *UpgradeApiHandler) handleRoutePostUpgradeOrg(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RoutePostUpgradeOrg(ctx)
}
func (f *UpgradeApiHandler) handleRouteGetOrgUpgrade(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteGetOrgUpgrade(ctx)
}
func (f *UpgradeApiHandler) handleRouteDeleteOrgUpgrade(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteDeleteOrgUpgrade(ctx)
}
func (f *UpgradeApiHandler) handleRoutePostUpgradeAlert(ctx *contextmodel.ReqContext, dashboardIdParam string, panelIdParam string) response.Response {
return f.svc.RoutePostUpgradeAlert(ctx, dashboardIdParam, panelIdParam)
}
func (f *UpgradeApiHandler) handleRoutePostUpgradeDashboard(ctx *contextmodel.ReqContext, dashboardIdParam string) response.Response {
return f.svc.RoutePostUpgradeDashboard(ctx, dashboardIdParam)
}
func (f *UpgradeApiHandler) handleRoutePostUpgradeAllDashboards(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RoutePostUpgradeAllDashboards(ctx)
}
func (f *UpgradeApiHandler) handleRoutePostUpgradeChannel(ctx *contextmodel.ReqContext, channelIdParam string) response.Response {
return f.svc.RoutePostUpgradeChannel(ctx, channelIdParam)
}
func (f *UpgradeApiHandler) handleRoutePostUpgradeAllChannels(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RoutePostUpgradeAllChannels(ctx)
}

View File

@@ -65,14 +65,6 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
return limits, nil
}
var alertOrgQuota int64
var alertGlobalQuota int64
if cfg.UnifiedAlerting.IsEnabled() {
alertOrgQuota = cfg.Quota.Org.AlertRule
alertGlobalQuota = cfg.Quota.Global.AlertRule
}
globalQuotaTag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.GlobalScope)
if err != nil {
return limits, err
@@ -82,7 +74,7 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
return limits, err
}
limits.Set(globalQuotaTag, alertGlobalQuota)
limits.Set(orgQuotaTag, alertOrgQuota)
limits.Set(globalQuotaTag, cfg.Quota.Global.AlertRule)
limits.Set(orgQuotaTag, cfg.Quota.Org.AlertRule)
return limits, nil
}

View File

@@ -751,66 +751,67 @@ func TestDashAlertMigration(t *testing.T) {
})
}
const newQueryModel = `{"datasource":{"type":"prometheus","uid":"gdev-prometheus"},"expr":"up{job=\"fake-data-gen\"}","instant":false,"interval":"%s","intervalMs":%d,"maxDataPoints":1500,"refId":"%s"}`
func createAlertQueryWithModel(refId string, ds string, from string, to string, model string) ngModels.AlertQuery {
rel, _ := getRelativeDuration(from, to)
return ngModels.AlertQuery{
RefID: refId,
RelativeTimeRange: ngModels.RelativeTimeRange{From: rel.From, To: rel.To},
DatasourceUID: ds,
Model: []byte(model),
}
}
func createAlertQuery(refId string, ds string, from string, to string) ngModels.AlertQuery {
dur, _ := calculateInterval(legacydata.NewDataTimeRange(from, to), simplejson.New(), nil)
return createAlertQueryWithModel(refId, ds, from, to, fmt.Sprintf(newQueryModel, "", dur.Milliseconds(), refId))
}
func createClassicConditionQuery(refId string, conditions []classicCondition) ngModels.AlertQuery {
exprModel := struct {
Type string `json:"type"`
RefID string `json:"refId"`
Conditions []classicCondition `json:"conditions"`
}{
"classic_conditions",
refId,
conditions,
}
exprModelJSON, _ := json.Marshal(&exprModel)
q := ngModels.AlertQuery{
RefID: refId,
DatasourceUID: expressionDatasourceUID,
Model: exprModelJSON,
}
// IntervalMS and MaxDataPoints are created PreSave by AlertQuery. They don't appear to be necessary for expressions,
// but run PreSave here to match the expected model.
_ = q.PreSave()
return q
}
func cond(refId string, reducer string, evalType string, thresh float64) classicCondition {
return classicCondition{
Evaluator: evaluator{Params: []float64{thresh}, Type: evalType},
Operator: struct {
Type string `json:"type"`
}{Type: "and"},
Query: struct {
Params []string `json:"params"`
}{Params: []string{refId}},
Reducer: struct {
Type string `json:"type"`
}{Type: reducer},
}
}
// TestDashAlertQueryMigration tests the execution of the migration specifically for alert rule queries.
func TestDashAlertQueryMigration(t *testing.T) {
sqlStore := db.InitTestDB(t)
x := sqlStore.GetEngine()
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
newQueryModel := `{"datasource":{"type":"prometheus","uid":"gdev-prometheus"},"expr":"up{job=\"fake-data-gen\"}","instant":false,"interval":"%s","intervalMs":%d,"maxDataPoints":1500,"refId":"%s"}`
createAlertQueryWithModel := func(refId string, ds string, from string, to string, model string) ngModels.AlertQuery {
rel, _ := getRelativeDuration(from, to)
return ngModels.AlertQuery{
RefID: refId,
RelativeTimeRange: ngModels.RelativeTimeRange{From: rel.From, To: rel.To},
DatasourceUID: ds,
Model: []byte(model),
}
}
createAlertQuery := func(refId string, ds string, from string, to string) ngModels.AlertQuery {
dur, _ := calculateInterval(legacydata.NewDataTimeRange(from, to), simplejson.New(), nil)
return createAlertQueryWithModel(refId, ds, from, to, fmt.Sprintf(newQueryModel, "", dur.Milliseconds(), refId))
}
createClassicConditionQuery := func(refId string, conditions []classicCondition) ngModels.AlertQuery {
exprModel := struct {
Type string `json:"type"`
RefID string `json:"refId"`
Conditions []classicCondition `json:"conditions"`
}{
"classic_conditions",
refId,
conditions,
}
exprModelJSON, _ := json.Marshal(&exprModel)
q := ngModels.AlertQuery{
RefID: refId,
DatasourceUID: expressionDatasourceUID,
Model: exprModelJSON,
}
// IntervalMS and MaxDataPoints are created PreSave by AlertQuery. They don't appear to be necessary for expressions,
// but run PreSave here to match the expected model.
_ = q.PreSave()
return q
}
cond := func(refId string, reducer string, evalType string, thresh float64) classicCondition {
return classicCondition{
Evaluator: evaluator{Params: []float64{thresh}, Type: evalType},
Operator: struct {
Type string `json:"type"`
}{Type: "and"},
Query: struct {
Params []string `json:"params"`
}{Params: []string{refId}},
Reducer: struct {
Type string `json:"type"`
}{Type: reducer},
}
}
genAlert := func(mutators ...ngModels.AlertRuleMutator) *ngModels.AlertRule {
rule := &ngModels.AlertRule{
ID: 1,

View File

@@ -1,7 +1,10 @@
package models
import (
"strings"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
@@ -11,17 +14,51 @@ import (
// Alertmanager is a helper struct for creating migrated alertmanager configs.
type Alertmanager struct {
Config *apiModels.PostableUserConfig
legacyRoute *apiModels.Route
config *apiModels.PostableUserConfig
legacyRoute *apiModels.Route
legacyReceiverToRoute map[string]*apiModels.Route
legacyUIDToReceiver map[string]*apiModels.PostableGrafanaReceiver
matcherRoute *dispatch.Route
}
// NewAlertmanager creates a new Alertmanager.
func NewAlertmanager() *Alertmanager {
c, r := createBaseConfig()
return &Alertmanager{
Config: c,
legacyRoute: r,
// FromPostableUserConfig creates an Alertmanager from a PostableUserConfig.
func FromPostableUserConfig(config *apiModels.PostableUserConfig) *Alertmanager {
if config == nil {
// No existing amConfig created from a previous migration.
config = createBaseConfig()
}
if config.AlertmanagerConfig.Route == nil {
// No existing base route created from a previous migration.
config.AlertmanagerConfig.Route = createDefaultRoute()
}
am := &Alertmanager{
config: config,
legacyRoute: getOrCreateNestedLegacyRoute(config),
legacyReceiverToRoute: make(map[string]*apiModels.Route),
legacyUIDToReceiver: config.GetGrafanaReceiverMap(),
matcherRoute: dispatch.NewRoute(config.AlertmanagerConfig.Route.AsAMRoute(), nil),
}
for _, r := range am.legacyRoute.Routes {
am.legacyReceiverToRoute[r.Receiver] = r
}
return am
}
// CleanAlertmanager removes the nested legacy route from the base PostableUserConfig if it's empty.
func CleanAlertmanager(am *Alertmanager) *apiModels.PostableUserConfig {
for i, r := range am.config.AlertmanagerConfig.Route.Routes {
if isNestedLegacyRoute(r) && len(r.Routes) == 0 {
// Remove empty nested route.
am.config.AlertmanagerConfig.Route.Routes = append(am.config.AlertmanagerConfig.Route.Routes[:i], am.config.AlertmanagerConfig.Route.Routes[i+1:]...)
return am.config
}
}
return am.config
}
func (am *Alertmanager) Match(lset model.LabelSet) []*dispatch.Route {
return dispatch.NewRoute(am.config.AlertmanagerConfig.Route.AsAMRoute(), nil).Match(lset)
}
// AddRoute adds a route to the alertmanager config.
@@ -29,7 +66,9 @@ func (am *Alertmanager) AddRoute(route *apiModels.Route) {
if route == nil {
return
}
am.legacyReceiverToRoute[route.Receiver] = route
am.legacyRoute.Routes = append(am.legacyRoute.Routes, route)
am.matcherRoute = dispatch.NewRoute(am.config.AlertmanagerConfig.Route.AsAMRoute(), nil)
}
// AddReceiver adds a receiver to the alertmanager config.
@@ -37,7 +76,7 @@ func (am *Alertmanager) AddReceiver(recv *apiModels.PostableGrafanaReceiver) {
if recv == nil {
return
}
am.Config.AlertmanagerConfig.Receivers = append(am.Config.AlertmanagerConfig.Receivers, &apiModels.PostableApiReceiver{
am.config.AlertmanagerConfig.Receivers = append(am.config.AlertmanagerConfig.Receivers, &apiModels.PostableApiReceiver{
Receiver: config.Receiver{
Name: recv.Name, // Channel name is unique within an Org.
},
@@ -45,12 +84,85 @@ func (am *Alertmanager) AddReceiver(recv *apiModels.PostableGrafanaReceiver) {
GrafanaManagedReceivers: []*apiModels.PostableGrafanaReceiver{recv},
},
})
am.legacyUIDToReceiver[recv.UID] = recv
}
// RemoveContactPointsAndRoutes removes all receivers and routes given legacy channel name.
func (am *Alertmanager) RemoveContactPointsAndRoutes(uid string) {
if recv, ok := am.legacyUIDToReceiver[uid]; ok {
for i, r := range am.config.AlertmanagerConfig.Receivers {
if r.Name == recv.Name {
am.config.AlertmanagerConfig.Receivers = append(am.config.AlertmanagerConfig.Receivers[:i], am.config.AlertmanagerConfig.Receivers[i+1:]...)
}
}
// Don't keep receiver and remove all nested routes that reference it.
// This will fail validation if the user has created other routes that reference this receiver.
// In that case, they must manually delete the added routes.
am.RemoveRoutes(recv.Name)
}
}
// RemoveRoutes legacy routes that send to the given receiver.
func (am *Alertmanager) RemoveRoutes(recv string) {
var keptRoutes []*apiModels.Route
for i, route := range am.legacyRoute.Routes {
if route.Receiver != recv {
keptRoutes = append(keptRoutes, am.legacyRoute.Routes[i])
}
}
delete(am.legacyReceiverToRoute, recv)
am.legacyRoute.Routes = keptRoutes
}
// GetLegacyRoute retrieves the legacy route for a given migrated channel UID.
func (am *Alertmanager) GetLegacyRoute(recv string) (*apiModels.Route, bool) {
route, ok := am.legacyReceiverToRoute[recv]
return route, ok
}
// GetReceiver retrieves the receiver for a given UID.
func (am *Alertmanager) GetReceiver(uid string) (*apiModels.PostableGrafanaReceiver, bool) {
recv, ok := am.legacyUIDToReceiver[uid]
return recv, ok
}
// GetContactLabel retrieves the label used to route for a given UID.
func (am *Alertmanager) GetContactLabel(uid string) string {
if recv, ok := am.GetReceiver(uid); ok {
if route, ok := am.GetLegacyRoute(recv.Name); ok {
for _, match := range route.ObjectMatchers {
if match.Type == labels.MatchEqual && strings.HasPrefix(match.Name, ngmodels.MigratedContactLabelPrefix) {
return match.Name
}
}
}
}
return ""
}
// getOrCreateNestedLegacyRoute finds or creates the nested route for migrated channels.
func getOrCreateNestedLegacyRoute(config *apiModels.PostableUserConfig) *apiModels.Route {
for _, r := range config.AlertmanagerConfig.Route.Routes {
if isNestedLegacyRoute(r) {
return r
}
}
nestedLegacyChannelRoute := createNestedLegacyRoute()
// Add new nested route as the first of the top-level routes.
config.AlertmanagerConfig.Route.Routes = append([]*apiModels.Route{nestedLegacyChannelRoute}, config.AlertmanagerConfig.Route.Routes...)
return nestedLegacyChannelRoute
}
// isNestedLegacyRoute checks whether a route is the nested legacy route for migrated channels.
func isNestedLegacyRoute(r *apiModels.Route) bool {
return len(r.ObjectMatchers) == 1 && r.ObjectMatchers[0].Name == ngmodels.MigratedUseLegacyChannelsLabel
}
// createBaseConfig creates an alertmanager config with the root-level route, default receiver, and nested route
// for migrated channels.
func createBaseConfig() (*apiModels.PostableUserConfig, *apiModels.Route) {
defaultRoute, nestedRoute := createDefaultRoute()
func createBaseConfig() *apiModels.PostableUserConfig {
defaultRoute := createDefaultRoute()
return &apiModels.PostableUserConfig{
AlertmanagerConfig: apiModels.PostableApiAlertingConfig{
Receivers: []*apiModels.PostableApiReceiver{
@@ -67,18 +179,18 @@ func createBaseConfig() (*apiModels.PostableUserConfig, *apiModels.Route) {
Route: defaultRoute,
},
},
}, nestedRoute
}
}
// createDefaultRoute creates a default root-level route and associated nested route that will contain all the migrated channels.
func createDefaultRoute() (*apiModels.Route, *apiModels.Route) {
func createDefaultRoute() *apiModels.Route {
nestedRoute := createNestedLegacyRoute()
return &apiModels.Route{
Receiver: "autogen-contact-point-default",
Routes: []*apiModels.Route{nestedRoute},
GroupByStr: []string{ngmodels.FolderTitleLabel, model.AlertNameLabel}, // To keep parity with pre-migration notifications.
RepeatInterval: nil,
}, nestedRoute
}
}
// createNestedLegacyRoute creates a nested route that will contain all the migrated channels.

View File

@@ -66,7 +66,7 @@ func (sync *sync) migratedFolder(ctx context.Context, l log.Logger, dashboardUID
dashFolder, err := sync.getFolder(ctx, folderID)
if err != nil {
// nolint:staticcheck
l.Warn("Failed to find folder for dashboard", "missing_folder_id", folderID, "error", err)
l.Warn("Failed to find folder for dashboard", "missingFolderId", folderID, "error", err)
}
if dashFolder != nil {
l = l.New("folderUid", dashFolder.UID, "folderName", dashFolder.Title)

View File

@@ -3,6 +3,7 @@ package migration
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
@@ -58,115 +59,369 @@ func (sync *sync) syncAndSaveState(
ctx context.Context,
dashboardUpgrades []*migmodels.DashboardUpgrade,
contactPairs []*migmodels.ContactPair,
) error {
delta := createDelta(dashboardUpgrades, contactPairs)
state, err := sync.syncDelta(ctx, delta)
skipExisting bool,
) (apiModels.OrgMigrationSummary, error) {
oldState, err := sync.migrationStore.GetOrgMigrationState(ctx, sync.orgID)
if err != nil {
return fmt.Errorf("sync state: %w", err)
return apiModels.OrgMigrationSummary{}, fmt.Errorf("get state: %w", err)
}
err = sync.migrationStore.SetOrgMigrationState(ctx, sync.orgID, state)
delta := createDelta(oldState, dashboardUpgrades, contactPairs, skipExisting)
summary, err := sync.syncDelta(ctx, oldState, delta)
if err != nil {
return fmt.Errorf("save state: %w", err)
return apiModels.OrgMigrationSummary{}, fmt.Errorf("sync state: %w", err)
}
return nil
err = sync.migrationStore.SetOrgMigrationState(ctx, sync.orgID, oldState)
if err != nil {
return apiModels.OrgMigrationSummary{}, fmt.Errorf("save state: %w", err)
}
return summary, nil
}
// StateDelta contains the changes to be made to the database based on the difference between
// existing migration state and new migration state.
type StateDelta struct {
DashboardsToAdd []*migmodels.DashboardUpgrade
ChannelsToAdd []*migmodels.ContactPair
AlertsToAdd []*migmodels.AlertPair
AlertsToDelete []*migmodels.AlertPair
DashboardsToAdd []*migmodels.DashboardUpgrade
DashboardsToDelete []*migrationStore.DashboardUpgrade
ChannelsToAdd []*migmodels.ContactPair
ChannelsToDelete []*migrationStore.ContactPair
}
// createDelta creates a StateDelta from the new dashboards upgrades and contact pairs.
// createDelta creates a StateDelta based on the difference between oldState and the new dashboardUpgrades and contactPairs.
// If skipExisting is true, existing alerts in each dashboard as well as existing channels will be not be deleted.
// If skipExisting is false, each given dashboard will be entirely replaced with the new state and all channels will be replaced.
func createDelta(
oldState *migrationStore.OrgMigrationState,
dashboardUpgrades []*migmodels.DashboardUpgrade,
contactPairs []*migmodels.ContactPair,
skipExisting bool,
) StateDelta {
return StateDelta{
DashboardsToAdd: dashboardUpgrades,
ChannelsToAdd: contactPairs,
delta := StateDelta{}
for _, du := range dashboardUpgrades {
oldDu, ok := oldState.MigratedDashboards[du.ID]
if !ok {
// Old state doesn't contain this dashboard, so add all alerts.
delta.DashboardsToAdd = append(delta.DashboardsToAdd, du)
continue
}
if !skipExisting {
delta.DashboardsToDelete = append(delta.DashboardsToDelete, oldDu)
delta.DashboardsToAdd = append(delta.DashboardsToAdd, du)
continue
}
for _, pair := range du.MigratedAlerts {
if _, ok := oldDu.MigratedAlerts[pair.LegacyRule.PanelID]; !ok {
// Only add alerts that don't already exist.
delta.AlertsToAdd = append(delta.AlertsToAdd, pair)
}
}
}
for _, newPair := range contactPairs {
oldPair, ok := oldState.MigratedChannels[newPair.Channel.ID]
if !ok || oldPair.NewReceiverUID == "" {
// Old state doesn't contain this channel, or this channel is a stub. We create a new contact point and route for it.
delta.ChannelsToAdd = append(delta.ChannelsToAdd, newPair)
continue
}
// Old state contains this channel, so it's either a re-migrated channel or a deleted channel.
if skipExisting {
// We're only migrating new channels, so we skip this one.
continue
}
delta.ChannelsToDelete = append(delta.ChannelsToDelete, oldPair)
delta.ChannelsToAdd = append(delta.ChannelsToAdd, newPair)
}
return delta
}
func summaryFromDelta(delta StateDelta) apiModels.OrgMigrationSummary {
summary := apiModels.OrgMigrationSummary{
NewDashboards: len(delta.DashboardsToAdd),
NewChannels: len(delta.ChannelsToAdd),
HasErrors: hasErrors(delta),
}
for _, du := range delta.DashboardsToAdd {
summary.NewAlerts += len(du.MigratedAlerts)
}
return summary
}
func hasErrors(delta StateDelta) bool {
for _, du := range delta.DashboardsToAdd {
for _, pair := range du.MigratedAlerts {
if pair.Error != nil {
return true
}
}
}
for _, pair := range delta.AlertsToAdd {
if pair.Error != nil {
return true
}
}
for _, pair := range delta.ChannelsToAdd {
if pair.Error != nil {
return true
}
}
return false
}
// syncDelta persists the given delta to the state and database.
func (sync *sync) syncDelta(ctx context.Context, delta StateDelta) (*migrationStore.OrgMigrationState, error) {
state := &migrationStore.OrgMigrationState{
OrgID: sync.orgID,
CreatedFolders: make([]string, 0),
func (sync *sync) syncDelta(ctx context.Context, state *migrationStore.OrgMigrationState, delta StateDelta) (apiModels.OrgMigrationSummary, error) {
amConfig, err := sync.handleAlertmanager(ctx, state, delta)
if err != nil {
return apiModels.OrgMigrationSummary{}, err
}
amConfig, err := sync.handleAlertmanager(ctx, delta)
err = sync.handleDeleteRules(ctx, state, delta)
if err != nil {
return nil, err
return apiModels.OrgMigrationSummary{}, err
}
err = sync.handleAddRules(ctx, state, delta, amConfig)
if err != nil {
return nil, err
return apiModels.OrgMigrationSummary{}, err
}
return state, nil
return summaryFromDelta(delta), nil
}
// handleAlertmanager persists the given channel delta to the state and database.
func (sync *sync) handleAlertmanager(ctx context.Context, delta StateDelta) (*migmodels.Alertmanager, error) {
amConfig := migmodels.NewAlertmanager()
func (sync *sync) handleAlertmanager(ctx context.Context, state *migrationStore.OrgMigrationState, delta StateDelta) (*migmodels.Alertmanager, error) {
cfg, err := sync.migrationStore.GetAlertmanagerConfig(ctx, sync.orgID)
if err != nil {
return nil, fmt.Errorf("get alertmanager config: %w", err)
}
amConfig := migmodels.FromPostableUserConfig(cfg)
if len(delta.ChannelsToAdd) == 0 {
if len(delta.ChannelsToDelete) == 0 && len(delta.ChannelsToAdd) == 0 {
return amConfig, nil
}
// Get the reverse relationship between rules and channels, so we can update labels on rules that reference modified channels.
rulesWithChannels := make(map[int64][]string)
for _, du := range state.MigratedDashboards {
for _, pair := range du.MigratedAlerts {
if pair.NewRuleUID != "" {
for _, id := range pair.ChannelIDs {
rulesWithChannels[id] = append(rulesWithChannels[id], pair.NewRuleUID)
}
}
}
}
// Information tracked to facilitate alert rule contact point label updates.
ruleToAddLabels := make(map[string][]string)
ruleToRemoveLabels := make(map[string][]string)
for _, pair := range delta.ChannelsToDelete {
delete(state.MigratedChannels, pair.LegacyID)
if pair.NewReceiverUID != "" {
label := amConfig.GetContactLabel(pair.NewReceiverUID)
for _, uid := range rulesWithChannels[pair.LegacyID] {
ruleToRemoveLabels[uid] = append(ruleToRemoveLabels[uid], label)
}
// Remove receivers and routes for channels that are being replaced.
amConfig.RemoveContactPointsAndRoutes(pair.NewReceiverUID)
}
}
for _, pair := range delta.ChannelsToAdd {
state.MigratedChannels[pair.Channel.ID] = newContactPair(pair)
amConfig.AddReceiver(pair.ContactPoint)
amConfig.AddRoute(pair.Route)
if pair.ContactPoint != nil {
for _, uid := range rulesWithChannels[pair.Channel.ID] {
ruleToAddLabels[uid] = append(ruleToAddLabels[uid], contactLabel(pair.ContactPoint.Name))
}
}
}
config := migmodels.CleanAlertmanager(amConfig)
// Validate the alertmanager configuration produced, this gives a chance to catch bad configuration at migration time.
// Validation between legacy and unified alerting can be different (e.g. due to bug fixes) so this would fail the migration in that case.
if err := sync.validateAlertmanagerConfig(amConfig.Config); err != nil {
if err := sync.validateAlertmanagerConfig(config); err != nil {
return nil, fmt.Errorf("validate AlertmanagerConfig: %w", err)
}
sync.log.Info("Writing alertmanager config", "receivers", len(amConfig.Config.AlertmanagerConfig.Receivers), "routes", len(amConfig.Config.AlertmanagerConfig.Route.Routes))
if err := sync.migrationStore.SaveAlertmanagerConfiguration(ctx, sync.orgID, amConfig.Config); err != nil {
sync.log.Info("Writing alertmanager config", "receivers", len(config.AlertmanagerConfig.Receivers), "routes", len(config.AlertmanagerConfig.Route.Routes))
if err := sync.migrationStore.SaveAlertmanagerConfiguration(ctx, sync.orgID, config); err != nil {
return nil, fmt.Errorf("write AlertmanagerConfig: %w", err)
}
// For the channels that have been changed, we need to update the labels on the alert rules that reference them.
err = sync.replaceLabels(ctx, ruleToAddLabels, ruleToRemoveLabels)
if err != nil {
return nil, fmt.Errorf("replace labels: %w", err)
}
return amConfig, nil
}
// handleAddRules persists the given add rule delta to the state and database.
func (sync *sync) handleAddRules(ctx context.Context, state *migrationStore.OrgMigrationState, delta StateDelta, amConfig *migmodels.Alertmanager) error {
pairs := make([]*migmodels.AlertPair, 0)
createdFolderUIDs := make(map[string]struct{})
for _, duToAdd := range delta.DashboardsToAdd {
pairsWithRules := make([]*migmodels.AlertPair, 0, len(duToAdd.MigratedAlerts))
for _, pair := range duToAdd.MigratedAlerts {
if pair.Rule != nil {
pairsWithRules = append(pairsWithRules, pair)
// replaceLabels replaces labels for the given alert rule UIDs.
func (sync *sync) replaceLabels(ctx context.Context, ruleToAddLabels map[string][]string, ruleToRemoveLabels map[string][]string) error {
var ruleUIDs []string
for uid := range ruleToAddLabels {
ruleUIDs = append(ruleUIDs, uid)
}
for uid := range ruleToRemoveLabels {
if _, ok := ruleToAddLabels[uid]; !ok {
ruleUIDs = append(ruleUIDs, uid)
}
}
ruleLabels, err := sync.migrationStore.GetRuleLabels(ctx, sync.orgID, ruleUIDs)
if err != nil {
return fmt.Errorf("get rule labels: %w", err)
}
for key, labels := range ruleLabels {
if labelsToRemove, ok := ruleToRemoveLabels[key.UID]; ok {
for _, label := range labelsToRemove {
delete(labels, label)
}
}
if labelsToAdd, ok := ruleToAddLabels[key.UID]; ok {
for _, label := range labelsToAdd {
labels[label] = "true"
}
}
err := sync.migrationStore.UpdateRuleLabels(ctx, key, labels)
if err != nil {
return fmt.Errorf("update rule labels: %w", err)
}
}
return nil
}
if len(pairsWithRules) > 0 {
l := sync.log.New("dashboardTitle", duToAdd.Title, "dashboardUid", duToAdd.UID)
migratedFolder, err := sync.migratedFolder(ctx, l, duToAdd.UID, duToAdd.FolderID)
if err != nil {
return err
// handleDeleteRules persists the given delete rule delta to the state and database.
func (sync *sync) handleDeleteRules(ctx context.Context, state *migrationStore.OrgMigrationState, delta StateDelta) error {
if len(delta.AlertsToDelete) == 0 && len(delta.DashboardsToDelete) == 0 {
return nil
}
// First we delete alerts so that empty folders can be deleted.
uids := make([]string, 0, len(delta.AlertsToDelete))
for _, pair := range delta.AlertsToDelete {
du, ok := state.MigratedDashboards[pair.LegacyRule.DashboardID]
if !ok {
return fmt.Errorf("dashboard '%d' not found in state", pair.LegacyRule.DashboardID)
}
delete(du.MigratedAlerts, pair.LegacyRule.PanelID)
if pair.Rule != nil {
uids = append(uids, pair.Rule.UID)
}
}
for _, du := range delta.DashboardsToDelete {
delete(state.MigratedDashboards, du.DashboardID)
for _, pair := range du.MigratedAlerts {
if pair.NewRuleUID != "" {
uids = append(uids, pair.NewRuleUID)
}
}
}
if len(uids) > 0 {
err := sync.migrationStore.DeleteAlertRules(ctx, sync.orgID, uids...)
if err != nil {
return fmt.Errorf("delete alert rules: %w", err)
}
// Attempt to delete folders that might be empty.
if len(delta.DashboardsToDelete) > 0 {
createdbyMigration := make(map[string]struct{}, len(state.CreatedFolders))
for _, uid := range state.CreatedFolders {
createdbyMigration[uid] = struct{}{}
}
// Keep track of folders created by the migration.
if _, exists := createdFolderUIDs[migratedFolder.uid]; migratedFolder.created && !exists {
createdFolderUIDs[migratedFolder.uid] = struct{}{}
state.CreatedFolders = append(state.CreatedFolders, migratedFolder.uid)
for _, du := range delta.DashboardsToDelete {
if _, ok := createdbyMigration[du.AlertFolderUID]; ok {
err := sync.migrationStore.DeleteFolders(ctx, sync.orgID, du.AlertFolderUID)
if err != nil {
if !errors.Is(err, migrationStore.ErrFolderNotDeleted) {
return fmt.Errorf("delete folder '%s': %w", du.AlertFolderUID, err)
}
sync.log.Info("Failed to delete folder during cleanup", "error", err)
} else {
delete(createdbyMigration, du.AlertFolderUID)
}
}
}
state.CreatedFolders = make([]string, 0, len(createdbyMigration))
for uid := range createdbyMigration {
state.CreatedFolders = append(state.CreatedFolders, uid)
}
}
}
return nil
}
// handleAddRules persists the given add rule delta to the state and database.
func (sync *sync) handleAddRules(ctx context.Context, state *migrationStore.OrgMigrationState, delta StateDelta, amConfig *migmodels.Alertmanager) error {
pairs := make([]*migmodels.AlertPair, 0, len(delta.AlertsToAdd))
for _, pair := range delta.AlertsToAdd {
du, ok := state.MigratedDashboards[pair.LegacyRule.DashboardID]
if !ok {
return fmt.Errorf("dashboard '%d' not found in state", pair.LegacyRule.DashboardID)
}
if pair.Rule != nil {
// These individually added alerts are created in the same folder as the existing ones.
pair.Rule.NamespaceUID = du.AlertFolderUID
pairs = append(pairs, pair)
}
du.MigratedAlerts[pair.LegacyRule.PanelID] = newAlertPair(pair)
}
if len(delta.DashboardsToAdd) > 0 {
createdFolderUIDs := make(map[string]struct{}, len(state.CreatedFolders))
for _, uid := range state.CreatedFolders {
createdFolderUIDs[uid] = struct{}{}
}
for _, duToAdd := range delta.DashboardsToAdd {
du := &migrationStore.DashboardUpgrade{
DashboardID: duToAdd.ID,
MigratedAlerts: make(map[int64]*migrationStore.AlertPair, len(duToAdd.MigratedAlerts)),
}
pairsWithRules := make([]*migmodels.AlertPair, 0, len(duToAdd.MigratedAlerts))
for _, pair := range duToAdd.MigratedAlerts {
du.MigratedAlerts[pair.LegacyRule.PanelID] = newAlertPair(pair)
if pair.Rule != nil {
pairsWithRules = append(pairsWithRules, pair)
}
}
for _, pair := range pairsWithRules {
pair.Rule.NamespaceUID = migratedFolder.uid
pairs = append(pairs, pair)
if len(pairsWithRules) > 0 {
l := sync.log.New("dashboardTitle", duToAdd.Title, "dashboardUid", duToAdd.UID)
migratedFolder, err := sync.migratedFolder(ctx, l, duToAdd.UID, duToAdd.FolderID)
if err != nil {
return err
}
du.AlertFolderUID = migratedFolder.uid
du.Warning = migratedFolder.warning
// Keep track of folders created by the migration.
if _, exists := createdFolderUIDs[migratedFolder.uid]; migratedFolder.created && !exists {
createdFolderUIDs[migratedFolder.uid] = struct{}{}
state.CreatedFolders = append(state.CreatedFolders, migratedFolder.uid)
}
for _, pair := range pairsWithRules {
pair.Rule.NamespaceUID = migratedFolder.uid
pairs = append(pairs, pair)
}
}
state.MigratedDashboards[du.DashboardID] = du
}
}
@@ -182,7 +437,7 @@ func (sync *sync) handleAddRules(ctx context.Context, state *migrationStore.OrgM
if err != nil {
return fmt.Errorf("deduplicate titles: %w", err)
}
rules, err := sync.attachContactPointLabels(ctx, pairs, amConfig)
rules, err := sync.attachContactPointLabels(ctx, state, pairs, amConfig)
if err != nil {
return fmt.Errorf("attach contact point labels: %w", err)
}
@@ -237,18 +492,31 @@ func (sync *sync) deduplicateTitles(ctx context.Context, pairs []*migmodels.Aler
}
// attachContactPointLabels attaches contact point labels to the given alert rules.
func (sync *sync) attachContactPointLabels(ctx context.Context, pairs []*migmodels.AlertPair, amConfig *migmodels.Alertmanager) ([]models.AlertRule, error) {
func (sync *sync) attachContactPointLabels(ctx context.Context, state *migrationStore.OrgMigrationState, pairs []*migmodels.AlertPair, amConfig *migmodels.Alertmanager) ([]models.AlertRule, error) {
rules := make([]models.AlertRule, 0, len(pairs))
for _, pair := range pairs {
l := sync.log.New("legacyRuleId", pair.LegacyRule.ID, "ruleUid", pair.Rule.UID)
alertChannels, err := sync.extractChannels(ctx, pair.LegacyRule)
if err != nil {
return nil, fmt.Errorf("extract channel IDs: %w", err)
}
statePair := state.MigratedDashboards[pair.LegacyRule.DashboardID].MigratedAlerts[pair.LegacyRule.PanelID]
statePair.ChannelIDs = make([]int64, 0, len(alertChannels))
for _, c := range alertChannels {
pair.Rule.Labels[contactLabel(c.Name)] = "true"
statePair.ChannelIDs = append(statePair.ChannelIDs, c.ID)
channelPair, ok := state.MigratedChannels[c.ID]
if ok {
label := amConfig.GetContactLabel(channelPair.NewReceiverUID)
if label != "" {
pair.Rule.Labels[label] = "true"
}
} else {
l.Warn("Failed to find migrated channel", "channel", c.Name)
// Creating stub so that when we eventually migrate the channel, we can update the labels on this rule.
state.MigratedChannels[c.ID] = &migrationStore.ContactPair{LegacyID: c.ID, Error: "channel not upgraded"}
}
}
rules = append(rules, *pair.Rule)
}
return rules, nil
@@ -316,3 +584,32 @@ func (sync *sync) validateAlertmanagerConfig(config *apiModels.PostableUserConfi
return nil
}
func newContactPair(pair *migmodels.ContactPair) *migrationStore.ContactPair {
p := &migrationStore.ContactPair{
LegacyID: pair.Channel.ID,
}
if pair.Error != nil {
p.Error = pair.Error.Error()
}
if pair.ContactPoint != nil {
p.NewReceiverUID = pair.ContactPoint.UID
}
return p
}
func newAlertPair(a *migmodels.AlertPair) *migrationStore.AlertPair {
pair := &migrationStore.AlertPair{}
if a.Error != nil {
pair.Error = a.Error.Error()
}
if a.LegacyRule != nil {
pair.LegacyID = a.LegacyRule.ID
pair.PanelID = a.LegacyRule.PanelID
}
if a.Rule != nil {
pair.NewRuleUID = a.Rule.UID
}
return pair
}

View File

@@ -6,11 +6,18 @@ import (
"fmt"
"time"
alertingModels "github.com/grafana/alerting/models"
v2 "github.com/prometheus/alertmanager/api/v2"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
)
@@ -18,8 +25,18 @@ import (
// actionName is the unique row-level lock name for serverlock.ServerLockService.
const actionName = "alerting migration"
var ErrUpgradeInProgress = errors.New("upgrade in progress")
type UpgradeService interface {
Run(ctx context.Context) error
MigrateAlert(ctx context.Context, orgID int64, dashboardID int64, panelID int64) (definitions.OrgMigrationSummary, error)
MigrateDashboardAlerts(ctx context.Context, orgID int64, dashboardID int64, skipExisting bool) (definitions.OrgMigrationSummary, error)
MigrateAllDashboardAlerts(ctx context.Context, orgID int64, skipExisting bool) (definitions.OrgMigrationSummary, error)
MigrateChannel(ctx context.Context, orgID int64, channelID int64) (definitions.OrgMigrationSummary, error)
MigrateAllChannels(ctx context.Context, orgID int64, skipExisting bool) (definitions.OrgMigrationSummary, error)
MigrateOrg(ctx context.Context, orgID int64, skipExisting bool) (definitions.OrgMigrationSummary, error)
GetOrgMigrationState(ctx context.Context, orgID int64) (*definitions.OrgMigrationState, error)
RevertOrg(ctx context.Context, orgID int64) error
}
type migrationService struct {
@@ -49,6 +66,273 @@ func ProvideService(
}, nil
}
type operation func(ctx context.Context) (*definitions.OrgMigrationSummary, error)
// verifyTry verifies that the org has been migrated, and then attempts to execute the operation. If another operation
// is already in progress, ErrUpgradeInProgress will be returned.
func (ms *migrationService) verifyTry(ctx context.Context, orgID int64, op operation) (definitions.OrgMigrationSummary, error) {
if err := ms.verifyMigrated(ctx, orgID); err != nil {
return definitions.OrgMigrationSummary{}, err
}
return ms.try(ctx, op)
}
// try attempts to execute the operation. If another operation is already in progress, ErrUpgradeInProgress will be returned.
func (ms *migrationService) try(ctx context.Context, op operation) (definitions.OrgMigrationSummary, error) {
var summary definitions.OrgMigrationSummary
var errOp error
errLock := ms.lock.LockExecuteAndRelease(ctx, actionName, time.Minute*10, func(ctx context.Context) {
errOp = ms.store.InTransaction(ctx, func(ctx context.Context) error {
s, err := op(ctx)
if err != nil {
return err
}
if s != nil {
summary.Add(*s)
}
return nil
})
})
if errLock != nil {
return definitions.OrgMigrationSummary{}, ErrUpgradeInProgress
}
if errOp != nil {
return definitions.OrgMigrationSummary{}, errOp
}
return summary, nil
}
// MigrateChannel migrates a single legacy notification channel to a unified alerting contact point.
func (ms *migrationService) MigrateChannel(ctx context.Context, orgID int64, channelID int64) (definitions.OrgMigrationSummary, error) {
return ms.verifyTry(ctx, orgID, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
summary := definitions.OrgMigrationSummary{}
om := ms.newOrgMigration(orgID)
oldState, err := om.migrationStore.GetOrgMigrationState(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get org migration state: %w", err)
}
channel, err := om.migrationStore.GetNotificationChannel(ctx, migrationStore.GetNotificationChannelQuery{OrgID: orgID, ID: channelID})
if err != nil && !errors.Is(err, migrationStore.ErrNotFound) {
return nil, fmt.Errorf("get notification channel: %w", err)
}
var delta StateDelta
if err != nil && errors.Is(err, migrationStore.ErrNotFound) {
// Notification channel no longer exists, delete this record from the state as well as delete any contacts points and routes.
om.log.Debug("Notification channel no longer exists", "channelId", channelID)
summary.Removed = true
pair, ok := oldState.MigratedChannels[channelID]
if !ok {
pair = &migrationStore.ContactPair{LegacyID: channelID}
}
delta = StateDelta{
ChannelsToDelete: []*migrationStore.ContactPair{pair},
}
} else {
pairs, err := om.migrateChannels([]*legacymodels.AlertNotification{channel})
if err != nil {
return nil, err
}
delta = createDelta(oldState, nil, pairs, false)
}
s, err := ms.newSync(orgID).syncDelta(ctx, oldState, delta)
if err != nil {
return nil, err
}
err = ms.migrationStore.SetOrgMigrationState(ctx, orgID, oldState)
if err != nil {
return nil, err
}
summary.Add(s)
return &summary, nil
})
}
// MigrateAllChannels migrates all legacy notification channel to unified alerting contact points.
func (ms *migrationService) MigrateAllChannels(ctx context.Context, orgID int64, skipExisting bool) (definitions.OrgMigrationSummary, error) {
return ms.verifyTry(ctx, orgID, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
summary := definitions.OrgMigrationSummary{}
om := ms.newOrgMigration(orgID)
pairs, err := om.migrateOrgChannels(ctx)
if err != nil {
return nil, err
}
s, err := ms.newSync(orgID).syncAndSaveState(ctx, nil, pairs, skipExisting)
if err != nil {
return nil, err
}
summary.Add(s)
return &summary, nil
})
}
// MigrateAlert migrates a single dashboard alert from legacy alerting to unified alerting.
func (ms *migrationService) MigrateAlert(ctx context.Context, orgID int64, dashboardID int64, panelID int64) (definitions.OrgMigrationSummary, error) {
return ms.verifyTry(ctx, orgID, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
summary := definitions.OrgMigrationSummary{}
om := ms.newOrgMigration(orgID)
oldState, err := om.migrationStore.GetOrgMigrationState(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get org migration state: %w", err)
}
delta := StateDelta{}
du, ok := oldState.MigratedDashboards[dashboardID]
if ok {
existingPair := &migmodels.AlertPair{LegacyRule: &legacymodels.Alert{PanelID: panelID, DashboardID: dashboardID}}
if pair, ok := du.MigratedAlerts[panelID]; ok {
existingPair.Rule = &models.AlertRule{UID: pair.NewRuleUID}
}
delta.AlertsToDelete = []*migmodels.AlertPair{existingPair}
}
alert, err := ms.migrationStore.GetDashboardAlert(ctx, orgID, dashboardID, panelID)
if err != nil && !errors.Is(err, migrationStore.ErrNotFound) {
return nil, fmt.Errorf("get alert: %w", err)
}
if err != nil && errors.Is(err, migrationStore.ErrNotFound) {
// Legacy alert no longer exists, delete this record from the state.
om.log.Debug("Alert no longer exists", "dashboardId", dashboardID, "panelId", panelID)
summary.Removed = true
} else {
newDu := om.migrateDashboard(ctx, dashboardID, []*legacymodels.Alert{alert})
if _, ok := oldState.MigratedDashboards[dashboardID]; !ok {
delta.DashboardsToAdd = []*migmodels.DashboardUpgrade{newDu}
} else {
// Replace only this alert on the dashboard.
for _, pair := range newDu.MigratedAlerts {
delta.AlertsToAdd = append(delta.AlertsToAdd, pair)
}
}
}
s, err := ms.newSync(orgID).syncDelta(ctx, oldState, delta)
if err != nil {
return nil, err
}
// We don't create new folders here, so no need to upgrade summary.CreatedFolders.
err = ms.migrationStore.SetOrgMigrationState(ctx, orgID, oldState)
if err != nil {
return nil, err
}
summary.Add(s)
return &summary, nil
})
}
// MigrateDashboardAlerts migrates all legacy dashboard alerts from a single dashboard to unified alerting.
func (ms *migrationService) MigrateDashboardAlerts(ctx context.Context, orgID int64, dashboardID int64, skipExisting bool) (definitions.OrgMigrationSummary, error) {
return ms.verifyTry(ctx, orgID, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
summary := definitions.OrgMigrationSummary{}
om := ms.newOrgMigration(orgID)
alerts, err := ms.migrationStore.GetDashboardAlerts(ctx, orgID, dashboardID)
if err != nil {
return nil, fmt.Errorf("get alerts: %w", err)
}
du := om.migrateDashboard(ctx, dashboardID, alerts)
s, err := ms.newSync(orgID).syncAndSaveState(ctx, []*migmodels.DashboardUpgrade{du}, nil, skipExisting)
if err != nil {
return nil, err
}
summary.Add(s)
return &summary, nil
})
}
// MigrateAllDashboardAlerts migrates all legacy alerts to unified alerting contact points.
func (ms *migrationService) MigrateAllDashboardAlerts(ctx context.Context, orgID int64, skipExisting bool) (definitions.OrgMigrationSummary, error) {
return ms.verifyTry(ctx, orgID, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
summary := definitions.OrgMigrationSummary{}
om := ms.newOrgMigration(orgID)
dashboardUpgrades, err := om.migrateOrgAlerts(ctx)
if err != nil {
return nil, err
}
s, err := ms.newSync(orgID).syncAndSaveState(ctx, dashboardUpgrades, nil, skipExisting)
if err != nil {
return nil, err
}
summary.Add(s)
return &summary, nil
})
}
// MigrateOrg executes the migration for a single org.
func (ms *migrationService) MigrateOrg(ctx context.Context, orgID int64, skipExisting bool) (definitions.OrgMigrationSummary, error) {
return ms.try(ctx, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
summary := definitions.OrgMigrationSummary{}
ms.log.Info("Starting legacy migration for org", "orgId", orgID, "skipExisting", skipExisting)
om := ms.newOrgMigration(orgID)
dashboardUpgrades, pairs, err := om.migrateOrg(ctx)
if err != nil {
return nil, err
}
s, err := ms.newSync(orgID).syncAndSaveState(ctx, dashboardUpgrades, pairs, skipExisting)
if err != nil {
return nil, err
}
err = ms.migrationStore.SetMigrated(ctx, orgID, true)
if err != nil {
return nil, fmt.Errorf("setting migration status: %w", err)
}
summary.Add(s)
return &summary, nil
})
}
// GetOrgMigrationState returns the current migration state for an org. This is a potentially expensive operation as it
// requires re-hydrating the entire migration state from the database against all current alerting resources.
func (ms *migrationService) GetOrgMigrationState(ctx context.Context, orgID int64) (*definitions.OrgMigrationState, error) {
if migrated, err := ms.migrationStore.IsMigrated(ctx, orgID); err != nil || !migrated {
return &definitions.OrgMigrationState{OrgID: orgID}, err
}
dState, err := ms.migrationStore.GetOrgMigrationState(ctx, orgID)
if err != nil {
return nil, err
}
cfg, err := ms.migrationStore.GetAlertmanagerConfig(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get alertmanager config: %w", err)
}
amConfig := migmodels.FromPostableUserConfig(cfg)
// Hydrate the slim database model.
migratedChannels, err := ms.fromContactPairs(ctx, orgID, dState.MigratedChannels, amConfig)
if err != nil {
return nil, fmt.Errorf("rehydrate channels: %w", err)
}
migratedDashboards, err := ms.fromDashboardUpgrades(ctx, orgID, dState.MigratedDashboards, amConfig)
if err != nil {
return nil, fmt.Errorf("rehydrate alerts: %w", err)
}
return &definitions.OrgMigrationState{
OrgID: dState.OrgID,
MigratedDashboards: migratedDashboards,
MigratedChannels: migratedChannels,
}, nil
}
// Run starts the migration to transition between legacy alerting and unified alerting based on the current and desired
// alerting type as determined by the kvstore and configuration, respectively.
func (ms *migrationService) Run(ctx context.Context) error {
@@ -197,7 +481,7 @@ func (ms *migrationService) migrateAllOrgs(ctx context.Context) error {
return fmt.Errorf("migrate org %d: %w", o.ID, migrationErr)
}
err = ms.newSync(o.ID).syncAndSaveState(ctx, dashboardUpgrades, contactPairs)
_, err = ms.newSync(o.ID).syncAndSaveState(ctx, dashboardUpgrades, contactPairs, false)
if err != nil {
return err
}
@@ -216,3 +500,298 @@ func (ms *migrationService) migrateAllOrgs(ctx context.Context) error {
}
return nil
}
// RevertOrg reverts the migration, deleting all unified alerting resources such as alert rules, alertmanager
// configurations, and silence files for a single organization.
// In addition, it will delete all folders and permissions originally created by this migration.
func (ms *migrationService) RevertOrg(ctx context.Context, orgID int64) error {
ms.log.Info("Reverting legacy migration for org", "orgId", orgID)
_, err := ms.try(ctx, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
return nil, ms.migrationStore.RevertOrg(ctx, orgID)
})
return err
}
// RevertAllOrgs reverts the migration for all orgs, deleting all unified alerting resources such as alert rules, alertmanager configurations, and silence files.
// In addition, it will delete all folders and permissions originally created by this migration.
func (ms *migrationService) RevertAllOrgs(ctx context.Context) error {
ms.log.Info("Reverting legacy migration for all orgs")
_, err := ms.try(ctx, func(ctx context.Context) (*definitions.OrgMigrationSummary, error) {
return nil, ms.migrationStore.RevertAllOrgs(ctx)
})
return err
}
// verifyMigrated returns an error if the org has not been migrated.
func (ms *migrationService) verifyMigrated(ctx context.Context, orgID int64) error {
migrated, err := ms.migrationStore.IsMigrated(ctx, orgID)
if err != nil {
return fmt.Errorf("check if migrated: %w", err)
}
if !migrated {
return fmt.Errorf("not migrated")
}
return nil
}
// fromDashboardUpgrades converts DashboardUpgrades to their api representation. This requires rehydrating information
// from the database for the current state of dashboards, alerts, and rules.
func (ms *migrationService) fromDashboardUpgrades(ctx context.Context, orgID int64, migratedDashboards map[int64]*migrationStore.DashboardUpgrade, amConfig *migmodels.Alertmanager) ([]*definitions.DashboardUpgrade, error) {
// We preload information in bulk for performance reasons.
alertRules, err := ms.migrationStore.GetSlimAlertRules(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get all alert rules: %w", err)
}
dashboardAlerts, err := ms.migrationStore.GetSlimOrgDashboardAlerts(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get dashboard alerts: %w", err)
}
dashIDInfo, err := ms.migrationStore.GetSlimDashboards(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get dashboards: %w", err)
}
dashUIDInfo := make(map[string]migrationStore.SlimDashboard, len(dashIDInfo))
for _, info := range dashIDInfo {
dashUIDInfo[info.UID] = info
}
res := make([]*definitions.DashboardUpgrade, 0, len(dashboardAlerts))
existingDashboards := make(map[int64]struct{})
for dashboardID, alerts := range dashboardAlerts {
existingDashboards[dashboardID] = struct{}{}
mDu := &definitions.DashboardUpgrade{
MigratedAlerts: make([]*definitions.AlertPair, 0),
DashboardID: dashboardID,
}
dashInfo, ok := dashIDInfo[dashboardID]
if ok {
folderInfo := dashIDInfo[dashInfo.FolderID]
mDu.DashboardUID = dashInfo.UID
mDu.DashboardName = dashInfo.Title
mDu.FolderUID = folderInfo.UID
mDu.FolderName = folderInfo.Title
mDu.Provisioned = dashInfo.Provisioned
}
du, ok := migratedDashboards[dashboardID]
if !ok {
mDu.Error = "dashboard not upgraded"
// Empty dashboard upgrade, to simplify logic below.
du = &migrationStore.DashboardUpgrade{
DashboardID: dashboardID,
MigratedAlerts: make(map[int64]*migrationStore.AlertPair),
}
}
mDu.Warning = du.Warning
if du.AlertFolderUID != "" {
newFolderInfo := dashUIDInfo[du.AlertFolderUID]
mDu.NewFolderUID = du.AlertFolderUID
mDu.NewFolderName = newFolderInfo.Title
}
existingAlerts := make(map[int64]struct{})
for _, a := range alerts {
existingAlerts[a.ID] = struct{}{}
pair := &definitions.AlertPair{
LegacyAlert: fromSlimAlert(a),
Error: "alert not upgraded",
}
if p, ok := du.MigratedAlerts[a.PanelID]; ok {
pair.Error = p.Error
if p.NewRuleUID != "" {
if rule, ok := alertRules[p.NewRuleUID]; ok {
var sendTo = make([]string, 0)
for _, m := range amConfig.Match(extractLabels(rule, mDu.NewFolderName)) {
sendTo = append(sendTo, m.RouteOpts.Receiver)
}
pair.AlertRule = fromSlimAlertRule(rule, sendTo)
} else {
// We could potentially set an error here, but it's not really an error. It just means that the
// user deleted the migrated rule after the migration. This could just as easily be intentional.
ms.log.Info("Could not find rule for migrated alert", "alertId", a.ID, "ruleUid", p.NewRuleUID)
}
}
}
mDu.MigratedAlerts = append(mDu.MigratedAlerts, pair)
}
// Now we check the inverse, for alerts that were migrated but no longer exist.
for _, p := range du.MigratedAlerts {
if _, ok := existingAlerts[p.LegacyID]; !ok {
pair := &definitions.AlertPair{
LegacyAlert: &definitions.LegacyAlert{ID: p.LegacyID, PanelID: p.PanelID, DashboardID: du.DashboardID},
Error: "alert no longer exists",
}
if p.NewRuleUID != "" {
if rule, ok := alertRules[p.NewRuleUID]; ok {
var sendTo = make([]string, 0)
for _, m := range amConfig.Match(extractLabels(rule, mDu.NewFolderName)) {
sendTo = append(sendTo, m.RouteOpts.Receiver)
}
pair.AlertRule = fromSlimAlertRule(rule, sendTo)
}
}
mDu.MigratedAlerts = append(mDu.MigratedAlerts, pair)
}
}
res = append(res, mDu)
}
// Now we check the inverse, for dashboards that were migrated but no longer exist.
for dashboardId, du := range migratedDashboards {
if _, ok := existingDashboards[dashboardId]; !ok {
mDu := &definitions.DashboardUpgrade{
MigratedAlerts: make([]*definitions.AlertPair, 0),
DashboardID: dashboardId,
Error: "dashboard no longer exists",
Warning: du.Warning,
}
if du.AlertFolderUID != "" {
newFolderInfo := dashUIDInfo[du.AlertFolderUID]
mDu.NewFolderUID = du.AlertFolderUID
mDu.NewFolderName = newFolderInfo.Title
}
for _, p := range du.MigratedAlerts {
pair := &definitions.AlertPair{
LegacyAlert: &definitions.LegacyAlert{ID: p.LegacyID, PanelID: p.PanelID, DashboardID: du.DashboardID},
Error: "dashboard no longer exists",
}
if p.NewRuleUID != "" {
if rule, ok := alertRules[p.NewRuleUID]; ok {
var sendTo = make([]string, 0)
for _, m := range amConfig.Match(extractLabels(rule, mDu.NewFolderName)) {
sendTo = append(sendTo, m.RouteOpts.Receiver)
}
pair.AlertRule = fromSlimAlertRule(rule, sendTo)
}
}
mDu.MigratedAlerts = append(mDu.MigratedAlerts, pair)
}
res = append(res, mDu)
}
}
return res, nil
}
// fromContactPairs converts ContactPairs to their api representation. This requires rehydrating information
// from the database for the current state of legacy channels and alertmanager configurations.
func (ms *migrationService) fromContactPairs(ctx context.Context, orgID int64, migratedChannels map[int64]*migrationStore.ContactPair, amConfig *migmodels.Alertmanager) ([]*definitions.ContactPair, error) {
channels, err := ms.migrationStore.GetNotificationChannels(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("get notification channels: %w", err)
}
res := make([]*definitions.ContactPair, 0, len(channels))
existingChannels := make(map[int64]struct{})
for _, channel := range channels {
existingChannels[channel.ID] = struct{}{}
pair := &definitions.ContactPair{
LegacyChannel: fromAlertNotification(channel),
Error: "channel not upgraded",
}
if p, ok := migratedChannels[channel.ID]; ok {
if p.NewReceiverUID != "" {
if recv, ok := amConfig.GetReceiver(p.NewReceiverUID); ok {
if route, ok := amConfig.GetLegacyRoute(recv.Name); ok {
pair.ContactPointUpgrade = fromContactPointUpgrade(recv, route)
}
}
}
pair.Error = p.Error
}
res = append(res, pair)
}
// Now we check the inverse, for channels that were migrated but no longer exist.
for _, p := range migratedChannels {
if _, ok := existingChannels[p.LegacyID]; !ok {
pair := &definitions.ContactPair{
LegacyChannel: &definitions.LegacyChannel{ID: p.LegacyID},
Error: "channel no longer exists",
}
if p.NewReceiverUID != "" {
if recv, ok := amConfig.GetReceiver(p.NewReceiverUID); ok {
if route, ok := amConfig.GetLegacyRoute(recv.Name); ok {
pair.ContactPointUpgrade = fromContactPointUpgrade(recv, route)
}
}
}
res = append(res, pair)
}
}
return res, nil
}
// fromSlimAlert converts a slim alert to the api representation.
func fromSlimAlert(alert *migrationStore.SlimAlert) *definitions.LegacyAlert {
if alert == nil {
return nil
}
return &definitions.LegacyAlert{
ID: alert.ID,
DashboardID: alert.DashboardID,
PanelID: alert.PanelID,
Name: alert.Name,
}
}
// fromSlimAlertRule converts a slim alert rule to the api representation.
func fromSlimAlertRule(rule *migrationStore.SlimAlertRule, sendsTo []string) *definitions.AlertRuleUpgrade {
if rule == nil {
return nil
}
return &definitions.AlertRuleUpgrade{
UID: rule.UID,
Title: rule.Title,
SendsTo: sendsTo,
}
}
// fromAlertNotification converts an alert notification to the api representation.
func fromAlertNotification(channel *legacymodels.AlertNotification) *definitions.LegacyChannel {
if channel == nil {
return nil
}
return &definitions.LegacyChannel{
ID: channel.ID,
Name: channel.Name,
Type: channel.Type,
}
}
// fromContactPointUpgrade converts a postable grafana receiver and route to the api representation.
func fromContactPointUpgrade(recv *definitions.PostableGrafanaReceiver, route *definitions.Route) *definitions.ContactPointUpgrade {
if recv == nil || len(route.ObjectMatchers) == 0 {
return nil
}
return &definitions.ContactPointUpgrade{
Name: recv.Name,
Type: recv.Type,
RouteMatchers: route.ObjectMatchers,
}
}
func extractLabels(rule *migrationStore.SlimAlertRule, folderName string) model.LabelSet {
mls := v2.APILabelSetToModelLabelSet(rule.Labels)
mls[alertingModels.NamespaceUIDLabel] = model.LabelValue(rule.NamespaceUID)
mls[model.AlertNameLabel] = model.LabelValue(rule.Title)
mls[alertingModels.RuleUIDLabel] = model.LabelValue(rule.UID)
mls[models.FolderTitleLabel] = model.LabelValue(folderName)
return mls
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,8 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
@@ -37,6 +39,8 @@ type Store interface {
// ReadStore is the database abstraction for read-only migration persistence.
type ReadStore interface {
GetAlertmanagerConfig(ctx context.Context, orgID int64) (*apimodels.PostableUserConfig, error)
GetAllOrgs(ctx context.Context) ([]*org.OrgDTO, error)
GetDatasource(ctx context.Context, datasourceID int64, user identity.Requester) (*datasources.DataSource, error)
@@ -44,6 +48,8 @@ type ReadStore interface {
GetNotificationChannels(ctx context.Context, orgID int64) ([]*legacymodels.AlertNotification, error)
GetNotificationChannel(ctx context.Context, q GetNotificationChannelQuery) (*legacymodels.AlertNotification, error)
GetDashboardAlert(ctx context.Context, orgID int64, dashboardID int64, panelID int64) (*legacymodels.Alert, error)
GetDashboardAlerts(ctx context.Context, orgID int64, dashboardID int64) ([]*legacymodels.Alert, error)
GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*legacymodels.Alert, int, error)
GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
@@ -57,9 +63,15 @@ type ReadStore interface {
GetCurrentAlertingType(ctx context.Context) (AlertingType, error)
GetOrgMigrationState(ctx context.Context, orgID int64) (*OrgMigrationState, error)
GetAlertRuleTitles(ctx context.Context, orgID int64, namespaceUIDs ...string) (map[string][]string, error) // NamespaceUID -> Titles
GetAlertRuleTitles(ctx context.Context, orgID int64, namespaceUIDs ...string) (map[string][]string, error) // NamespaceUID -> Titles
GetRuleLabels(ctx context.Context, orgID int64, ruleUIDs []string) (map[models.AlertRuleKeyWithVersion]data.Labels, error) // Rule UID -> Labels
CaseInsensitive() bool
// Slims for performance optimization of GetOrgSummary rehydration.
GetSlimDashboards(ctx context.Context, orgID int64) (map[int64]SlimDashboard, error)
GetSlimOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*SlimAlert, error)
GetSlimAlertRules(ctx context.Context, orgID int64) (map[string]*SlimAlertRule, error)
}
// WriteStore is the database abstraction for write migration persistence.
@@ -77,7 +89,13 @@ type WriteStore interface {
SetCurrentAlertingType(ctx context.Context, t AlertingType) error
SetOrgMigrationState(ctx context.Context, orgID int64, summary *OrgMigrationState) error
RevertOrg(ctx context.Context, orgID int64) error
RevertAllOrgs(ctx context.Context) error
DeleteAlertRules(ctx context.Context, orgID int64, alertRuleUIDs ...string) error
DeleteFolders(ctx context.Context, orgID int64, uids ...string) error
UpdateRuleLabels(ctx context.Context, key models.AlertRuleKeyWithVersion, labels data.Labels) error
}
type migrationStore struct {
@@ -219,7 +237,11 @@ func (ms *migrationStore) GetOrgMigrationState(ctx context.Context, orgID int64)
}
if !exists {
return &OrgMigrationState{OrgID: orgID}, nil
return &OrgMigrationState{
OrgID: orgID,
MigratedDashboards: make(map[int64]*DashboardUpgrade),
MigratedChannels: make(map[int64]*ContactPair),
}, nil
}
var state OrgMigrationState
@@ -228,6 +250,14 @@ func (ms *migrationStore) GetOrgMigrationState(ctx context.Context, orgID int64)
return nil, err
}
if state.MigratedChannels == nil {
state.MigratedChannels = make(map[int64]*ContactPair)
}
if state.MigratedDashboards == nil {
state.MigratedDashboards = make(map[int64]*DashboardUpgrade)
}
return &state, nil
}
@@ -272,6 +302,47 @@ func (ms *migrationStore) GetAlertRuleTitles(ctx context.Context, orgID int64, n
return res, err
}
// GetRuleLabels returns a map of rule UID / version -> labels for all given org and rule uids. Version is needed to
// update alert rules because of their optimistic locking.
func (ms *migrationStore) GetRuleLabels(ctx context.Context, orgID int64, ruleUIDs []string) (map[models.AlertRuleKeyWithVersion]data.Labels, error) {
res := make(map[models.AlertRuleKeyWithVersion]data.Labels, len(ruleUIDs))
err := ms.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
type label struct {
models.AlertRuleKeyWithVersion `xorm:"extends"`
Labels data.Labels
}
labels := make([]label, 0)
err := sess.Table("alert_rule").Cols("uid", "org_id", "version", "labels").Where("org_id = ?", orgID).In("uid", ruleUIDs).Find(&labels)
if err != nil {
return err
}
for _, l := range labels {
res[l.AlertRuleKeyWithVersion] = l.Labels
}
return nil
})
return res, err
}
// UpdateRuleLabels updates the labels of an alert rule. Version is needed to update alert rules because of optimistic locking.
func (ms *migrationStore) UpdateRuleLabels(ctx context.Context, key models.AlertRuleKeyWithVersion, labels data.Labels) error {
return ms.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
affected, err := sess.Cols("labels").Where("org_id = ? AND uid = ?", key.OrgID, key.UID).Update(&models.AlertRule{
Version: key.Version,
Labels: labels,
})
if err != nil {
return err
}
if affected == 0 {
return fmt.Errorf("rule with uid %v not found", key)
}
return nil
})
}
// BATCHSIZE is a reasonable SQL batch size to prevent hitting placeholder limits (such as Error 1390 in MySQL) or packet size limits.
const BATCHSIZE = 1000
@@ -295,6 +366,35 @@ func (ms *migrationStore) InsertAlertRules(ctx context.Context, rules ...models.
return nil
}
// DeleteAlertRules deletes alert rules in a given org by their UIDs.
func (ms *migrationStore) DeleteAlertRules(ctx context.Context, orgID int64, alertRuleUIDs ...string) error {
batches := batchBy(alertRuleUIDs, BATCHSIZE)
for _, batch := range batches {
err := ms.alertingStore.DeleteAlertRulesByUID(ctx, orgID, batch...)
if err != nil {
return err
}
}
return nil
}
// GetAlertmanagerConfig returns the alertmanager configuration for the given org.
func (ms *migrationStore) GetAlertmanagerConfig(ctx context.Context, orgID int64) (*apimodels.PostableUserConfig, error) {
amConfig, err := ms.alertingStore.GetLatestAlertmanagerConfiguration(ctx, orgID)
if err != nil && !errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return nil, err
}
if amConfig == nil {
return nil, nil
}
cfg, err := notifier.Load([]byte(amConfig.AlertmanagerConfiguration))
if err != nil {
return nil, err
}
return cfg, nil
}
// SaveAlertmanagerConfiguration saves the alertmanager configuration for the given org.
func (ms *migrationStore) SaveAlertmanagerConfiguration(ctx context.Context, orgID int64, amConfig *apimodels.PostableUserConfig) error {
rawAmConfig, err := json.Marshal(amConfig)
@@ -318,6 +418,62 @@ var revertPermissions = []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
}
// RevertOrg reverts the migration for a given org, deleting all unified alerting resources such as alert rules and alertmanager
// configurations as well as all other database resources created during the migration, such as folders.
func (ms *migrationStore) RevertOrg(ctx context.Context, orgID int64) error {
return ms.store.InTransaction(ctx, func(ctx context.Context) error {
return ms.store.WithDbSession(ctx, func(sess *db.Session) error {
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ?", orgID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ?", orgID); err != nil {
return err
}
state, err := ms.GetOrgMigrationState(ctx, orgID)
if err != nil {
return err
}
if err := ms.DeleteFolders(ctx, orgID, state.CreatedFolders...); err != nil {
ms.log.Warn("Failed to delete migrated folders", "orgId", orgID, "err", err)
}
if _, err := sess.Exec("DELETE FROM alert_configuration WHERE org_id = ?", orgID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM ngalert_configuration WHERE org_id = ?", orgID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ?", orgID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM kv_store WHERE namespace = ? AND org_id = ?", notifier.KVNamespace, orgID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM kv_store WHERE namespace = ? AND org_id = ?", KVNamespace, orgID); err != nil {
return err
}
files, err := filepath.Glob(filepath.Join(ms.cfg.DataPath, "alerting", strconv.FormatInt(orgID, 10), "silences"))
if err != nil {
return err
}
for _, f := range files {
if err := os.Remove(f); err != nil {
ms.log.Error("Failed to remove silence file", "file", f, "err", err)
}
}
return nil
})
})
}
// RevertAllOrgs reverts the migration, deleting all unified alerting resources such as alert rules, alertmanager configurations, and silence files.
// In addition, it will delete all folders and permissions originally created by this migration, as well as the various migration statuses stored
// in kvstore, both org-specific and anyOrg.
@@ -342,7 +498,7 @@ func (ms *migrationStore) RevertAllOrgs(ctx context.Context) error {
return err
}
if err := ms.DeleteFolders(ctx, o.ID, state.CreatedFolders...); err != nil {
ms.log.Warn("Failed to delete migrated folders", "orgID", o.ID, "err", err)
ms.log.Warn("Failed to delete migrated folders", "orgId", o.ID, "err", err)
continue
}
}
@@ -504,6 +660,39 @@ func (ms *migrationStore) GetNotificationChannel(ctx context.Context, q GetNotif
return &res, err
}
// GetDashboardAlert loads a single legacy dashboard alerts for the given org and alert id.
func (ms *migrationStore) GetDashboardAlert(ctx context.Context, orgID int64, dashboardID int64, panelID int64) (*legacymodels.Alert, error) {
var dashAlert legacymodels.Alert
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
has, err := sess.SQL("select * from alert WHERE org_id = ? AND dashboard_id = ? AND panel_id = ?", orgID, dashboardID, panelID).Get(&dashAlert)
if err != nil {
return err
}
if !has {
return ErrNotFound
}
return nil
})
if err != nil {
return nil, err
}
return &dashAlert, nil
}
// GetDashboardAlerts loads all legacy dashboard alerts for the given org and dashboard.
func (ms *migrationStore) GetDashboardAlerts(ctx context.Context, orgID int64, dashboardID int64) ([]*legacymodels.Alert, error) {
var dashAlerts []*legacymodels.Alert
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL("select * from alert WHERE org_id = ? AND dashboard_id = ?", orgID, dashboardID).Find(&dashAlerts)
})
if err != nil {
return nil, err
}
return dashAlerts, nil
}
// GetOrgDashboardAlerts loads all legacy dashboard alerts for the given org mapped by dashboard id.
func (ms *migrationStore) GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*legacymodels.Alert, int, error) {
var dashAlerts []*legacymodels.Alert
@@ -552,3 +741,66 @@ func (ms *migrationStore) MapActions(permission accesscontrol.ResourcePermission
func (ms *migrationStore) CaseInsensitive() bool {
return ms.store.GetDialect().SupportEngine()
}
// GetSlimDashboards returns a map of dashboard id -> SlimDashboard for all dashboards in the given org. This is used as a
// performance optimization to avoid loading the full dashboards in bulk.
func (ms *migrationStore) GetSlimDashboards(ctx context.Context, orgID int64) (map[int64]SlimDashboard, error) {
res := make(map[int64]SlimDashboard)
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
dashes := make([]SlimDashboard, 0)
err := sess.Table("dashboard").Alias("d").Select("d.id, d.uid, d.title, d.folder_id, dashboard_provisioning.id IS NOT NULL as provisioned").
Join("LEFT", "dashboard_provisioning", `d.id = dashboard_provisioning.dashboard_id`).
Where("org_id = ?", orgID).Find(&dashes)
if err != nil {
return err
}
for _, d := range dashes {
res[d.ID] = d
}
res[0] = SlimDashboard{ID: 0, Title: folder.GeneralFolder.Title}
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
// GetSlimOrgDashboardAlerts returns a map of dashboard id -> SlimAlert for all alerts in the given org. This is used as a
// performance optimization to avoid loading the full alerts in bulk.
func (ms *migrationStore) GetSlimOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*SlimAlert, error) {
res := make(map[int64][]*SlimAlert)
rules := make([]*SlimAlert, 0)
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
err := sess.Table("alert").Cols("id", "dashboard_id", "panel_id", "name").Where("org_id = ?", orgID).Find(&rules)
if err != nil {
return err
}
for _, r := range rules {
res[r.DashboardID] = append(res[r.DashboardID], r)
}
return nil
})
return res, err
}
// GetSlimAlertRules returns a map of rule UID -> SlimAlertRule for all alert rules in the given org. This is used as a
// performance optimization to avoid loading the full alert rules in bulk.
func (ms *migrationStore) GetSlimAlertRules(ctx context.Context, orgID int64) (map[string]*SlimAlertRule, error) {
res := make(map[string]*SlimAlertRule)
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
rules := make([]*SlimAlertRule, 0)
err := sess.Table("alert_rule").Cols("uid", "title", "namespace_uid", "labels").Where("org_id = ?", orgID).Find(&rules)
if err != nil {
return err
}
for _, r := range rules {
res[r.UID] = r
}
return nil
})
return res, err
}

View File

@@ -0,0 +1,26 @@
package store
// SlimAlert is a slimmed down version of the legacy alert model.
type SlimAlert struct {
ID int64 `xorm:"id"`
DashboardID int64 `xorm:"dashboard_id"`
PanelID int64 `xorm:"panel_id"`
Name string
}
// SlimAlertRule is a slimmed down version of the alert rule model.
type SlimAlertRule struct {
UID string `xorm:"uid"`
Title string
NamespaceUID string `xorm:"namespace_uid"`
Labels map[string]string
}
// SlimDashboard is a slimmed down version of the dashboard model.
type SlimDashboard struct {
ID int64 `xorm:"id"`
FolderID int64 `xorm:"folder_id"`
UID string `xorm:"uid"`
Title string
Provisioned bool
}

View File

@@ -1,7 +1,30 @@
package store
// OrgMigrationState contains basic information about the state of an org migration.
// OrgMigrationState contains information about the state of an org migration.
type OrgMigrationState struct {
OrgID int64 `json:"orgId"`
CreatedFolders []string `json:"createdFolders"`
OrgID int64 `json:"orgId"`
MigratedDashboards map[int64]*DashboardUpgrade `json:"migratedDashboards"`
MigratedChannels map[int64]*ContactPair `json:"migratedChannels"`
CreatedFolders []string `json:"createdFolders"`
}
type DashboardUpgrade struct {
DashboardID int64 `json:"dashboardId"`
AlertFolderUID string `json:"alertFolderUid"`
MigratedAlerts map[int64]*AlertPair `json:"migratedAlerts"`
Warning string `json:"warning,omitempty"`
}
type AlertPair struct {
LegacyID int64 `json:"legacyId"`
PanelID int64 `json:"panelId"`
NewRuleUID string `json:"newRuleUid"`
ChannelIDs []int64 `json:"channelIds"`
Error string `json:"error,omitempty"`
}
type ContactPair struct {
LegacyID int64 `json:"legacyId"`
NewReceiverUID string `json:"newReceiverUid"`
Error string `json:"error,omitempty"`
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
fake_secrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -40,3 +41,43 @@ func (ms *fakeMigrationService) Run(_ context.Context) error {
// Do nothing.
return nil
}
func (ms *fakeMigrationService) MigrateAlert(ctx context.Context, orgID int64, dashboardID int64, panelID int64) (apimodels.OrgMigrationSummary, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) MigrateDashboardAlerts(ctx context.Context, orgID int64, dashboardID int64, skipExisting bool) (apimodels.OrgMigrationSummary, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) MigrateAllDashboardAlerts(ctx context.Context, orgID int64, skipExisting bool) (apimodels.OrgMigrationSummary, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) MigrateChannel(ctx context.Context, orgID int64, channelID int64) (apimodels.OrgMigrationSummary, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) MigrateAllChannels(ctx context.Context, orgID int64, skipExisting bool) (apimodels.OrgMigrationSummary, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) MigrateOrg(ctx context.Context, orgID int64, skipExisting bool) (apimodels.OrgMigrationSummary, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) GetOrgMigrationState(ctx context.Context, orgID int64) (*apimodels.OrgMigrationState, error) {
//TODO implement me
panic("implement me")
}
func (ms *fakeMigrationService) RevertOrg(ctx context.Context, orgID int64) error {
//TODO implement me
panic("implement me")
}

View File

@@ -339,6 +339,7 @@ func (ng *AlertNG) init() error {
Historian: history,
Hooks: api.NewHooks(ng.Log),
Tracer: ng.tracer,
UpgradeService: ng.upgradeService,
}
ng.api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())
@@ -373,7 +374,16 @@ func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleSt
// shouldRun determines if AlertNG should init or run anything more than just the migration.
func (ng *AlertNG) shouldRun() bool {
return ng.Cfg.UnifiedAlerting.IsEnabled()
if ng.Cfg.UnifiedAlerting.IsEnabled() {
return true
}
// Feature flag will preview UA alongside legacy, so that UA routes are registered but the scheduler remains disabled.
if ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingPreviewUpgrade) {
return true
}
return false
}
// Run starts the scheduler and Alertmanager.
@@ -392,7 +402,8 @@ func (ng *AlertNG) Run(ctx context.Context) error {
return ng.AlertsRouter.Run(subCtx)
})
if ng.Cfg.UnifiedAlerting.ExecuteAlerts {
// We explicitly check that UA is enabled here in case FlagAlertingPreviewUpgrade is enabled but UA is disabled.
if ng.Cfg.UnifiedAlerting.ExecuteAlerts && ng.Cfg.UnifiedAlerting.IsEnabled() {
// Only Warm() the state manager if we are actually executing alerts.
// Doing so when we are not executing alerts is wasteful and could lead
// to misleading rule status queries, as the status returned will be

View File

@@ -36,19 +36,13 @@ func (cfg *Cfg) readQuotaSettings() {
quota := cfg.Raw.Section("quota")
cfg.Quota.Enabled = quota.Key("enabled").MustBool(false)
var alertOrgQuota int64
var alertGlobalQuota int64
if cfg.UnifiedAlerting.IsEnabled() {
alertOrgQuota = quota.Key("org_alert_rule").MustInt64(100)
alertGlobalQuota = quota.Key("global_alert_rule").MustInt64(-1)
}
// per ORG Limits
cfg.Quota.Org = OrgQuota{
User: quota.Key("org_user").MustInt64(10),
DataSource: quota.Key("org_data_source").MustInt64(10),
Dashboard: quota.Key("org_dashboard").MustInt64(10),
ApiKey: quota.Key("org_api_key").MustInt64(10),
AlertRule: alertOrgQuota,
AlertRule: quota.Key("org_alert_rule").MustInt64(100),
}
// per User limits
@@ -65,7 +59,7 @@ func (cfg *Cfg) readQuotaSettings() {
ApiKey: quota.Key("global_api_key").MustInt64(-1),
Session: quota.Key("global_session").MustInt64(-1),
File: quota.Key("global_file").MustInt64(-1),
AlertRule: alertGlobalQuota,
AlertRule: quota.Key("global_alert_rule").MustInt64(-1),
Correlations: quota.Key("global_correlations").MustInt64(-1),
}
}

View File

@@ -11999,6 +11999,20 @@
}
}
},
"AlertPair": {
"type": "object",
"properties": {
"alertRule": {
"$ref": "#/definitions/AlertRuleUpgrade"
},
"error": {
"type": "string"
},
"legacyAlert": {
"$ref": "#/definitions/LegacyAlert"
}
}
},
"AlertQuery": {
"type": "object",
"title": "AlertQuery represents a single query associated with an alert definition.",
@@ -12183,6 +12197,23 @@
}
}
},
"AlertRuleUpgrade": {
"type": "object",
"properties": {
"sendsTo": {
"type": "array",
"items": {
"type": "string"
}
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
}
},
"AlertStateInfoDTO": {
"type": "object",
"properties": {
@@ -13097,6 +13128,20 @@
}
}
},
"ContactPair": {
"type": "object",
"properties": {
"contactPoint": {
"$ref": "#/definitions/ContactPointUpgrade"
},
"error": {
"type": "string"
},
"legacyChannel": {
"$ref": "#/definitions/LegacyChannel"
}
}
},
"ContactPointExport": {
"type": "object",
"title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.",
@@ -13116,6 +13161,20 @@
}
}
},
"ContactPointUpgrade": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"routeMatchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"type": {
"type": "string"
}
}
},
"ContactPoints": {
"type": "array",
"items": {
@@ -13860,6 +13919,48 @@
}
}
},
"DashboardUpgrade": {
"type": "object",
"properties": {
"dashboardId": {
"type": "integer",
"format": "int64"
},
"dashboardName": {
"type": "string"
},
"dashboardUid": {
"type": "string"
},
"error": {
"type": "string"
},
"folderName": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"migratedAlerts": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertPair"
}
},
"newFolderName": {
"type": "string"
},
"newFolderUid": {
"type": "string"
},
"provisioned": {
"type": "boolean"
},
"warning": {
"type": "string"
}
}
},
"DashboardVersionMeta": {
"description": "DashboardVersionMeta extends the DashboardVersionDTO with the names\nassociated with the UserIds, overriding the field with the same name from\nthe DashboardVersionDTO model.",
"type": "object",
@@ -15868,6 +15969,21 @@
}
}
},
"LegacyChannel": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"LibraryElementArrayResponse": {
"type": "object",
"title": "LibraryElementArrayResponse is a response struct for an array of LibraryElementDTO.",
@@ -16645,6 +16761,50 @@
}
}
},
"OrgMigrationState": {
"type": "object",
"properties": {
"migratedChannels": {
"type": "array",
"items": {
"$ref": "#/definitions/ContactPair"
}
},
"migratedDashboards": {
"type": "array",
"items": {
"$ref": "#/definitions/DashboardUpgrade"
}
},
"orgId": {
"type": "integer",
"format": "int64"
}
}
},
"OrgMigrationSummary": {
"type": "object",
"properties": {
"hasErrors": {
"type": "boolean"
},
"newAlerts": {
"type": "integer",
"format": "int64"
},
"newChannels": {
"type": "integer",
"format": "int64"
},
"newDashboards": {
"type": "integer",
"format": "int64"
},
"removed": {
"type": "boolean"
}
}
},
"OrgUserDTO": {
"type": "object",
"properties": {
@@ -21194,6 +21354,7 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"$ref": "#/definitions/alertGroup"
@@ -21442,6 +21603,7 @@
}
},
"integration": {
"description": "Integration integration",
"type": "object",
"required": [
"name",
@@ -21585,7 +21747,6 @@
}
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",
@@ -21651,7 +21812,6 @@
}
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"active",

View File

@@ -57,6 +57,8 @@ export function getNavTitle(navId: string | undefined) {
return t('nav.oncall.title', 'OnCall');
case 'alerting-legacy':
return t('nav.alerting-legacy.title', 'Alerting (legacy)');
case 'alerting-upgrade':
return t('nav.alerting-upgrade.title', 'Alerting upgrade');
case 'alert-home':
return t('nav.alerting-home.title', 'Home');
case 'alert-list':
@@ -203,6 +205,11 @@ export function getNavSubTitle(navId: string | undefined) {
return t('nav.library-panels.subtitle', 'Reusable panels that can be added to multiple dashboards');
case 'alerting':
return t('nav.alerting.subtitle', 'Learn about problems in your systems moments after they occur');
case 'alerting-upgrade':
return t(
'nav.alerting-upgrade.subtitle',
'Upgrade your existing legacy alerts and notification channels to the new Grafana Alerting'
);
case 'alert-list':
return t('nav.alerting-list.subtitle', 'Rules that determine whether an alert will fire');
case 'receivers':

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
import { css } from '@emotion/css';
import React, { HTMLAttributes } from 'react';
import { useLocalStorage } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, AlertVariant, Button, HorizontalGroup, Tooltip, useTheme2 } from '@grafana/ui';
import { getIconFromSeverity } from '@grafana/ui/src/components/Alert/Alert';
type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center';
interface CollapsibleAlertProps extends HTMLAttributes<HTMLDivElement> {
localStoreKey: string;
startClosed?: boolean;
severity?: AlertVariant;
collapseText?: string;
collapseTooltip: string;
collapseJustify?: Justify;
alertTitle: string;
children?: React.ReactNode;
}
export const CollapsibleAlert = ({
localStoreKey,
startClosed = false,
severity = 'error',
collapseText,
collapseTooltip,
collapseJustify = 'flex-end',
alertTitle,
children,
}: CollapsibleAlertProps) => {
const theme = useTheme2();
const styles = getStyles(theme, severity);
const [closed, setClosed] = useLocalStorage(localStoreKey, startClosed);
return (
<>
{closed && (
<HorizontalGroup justify={collapseJustify}>
<Tooltip content={collapseTooltip} placement="bottom">
<Button
fill="text"
variant="secondary"
icon={getIconFromSeverity(severity)}
className={styles.warningButton}
onClick={() => setClosed(false)}
>
{collapseText}
</Button>
</Tooltip>
</HorizontalGroup>
)}
{!closed && (
<Alert severity={severity} title={alertTitle} onRemove={() => setClosed(true)}>
{children}
</Alert>
)}
</>
);
};
const getStyles = (theme: GrafanaTheme2, severity: AlertVariant) => {
const color = theme.colors[severity];
return {
warningButton: css({
color: color.text,
'&:hover': {
background: color.transparent,
},
}),
};
};

View File

@@ -14,7 +14,7 @@ const DeprecationNotice = () => (
</p>
<p>
See{' '}
<a href="https://grafana.com/docs/grafana/latest/alerting/migrating-alerts/">
<a href="https://grafana.com/docs/grafana/latest/alerting/set-up/migrating-alerts/">
how to upgrade to Grafana Alerting
</a>{' '}
to learn more.

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { TextLink } from '@grafana/ui';
import { CollapsibleAlert } from './CollapsibleAlert';
const LOCAL_STORAGE_KEY = 'grafana.unifiedalerting.upgrade.previewNotice';
export const UAPreviewNotice = () => {
if (config.unifiedAlertingEnabled || !config.featureToggles.alertingPreviewUpgrade) {
return null;
}
return (
<CollapsibleAlert
localStoreKey={LOCAL_STORAGE_KEY}
alertTitle={'This is a preview of the upgraded Grafana Alerting'}
collapseText={'Grafana Alerting Preview'}
collapseTooltip={'Show preview warning'}
severity={'warning'}
>
<p>
No rules are being evaluated and legacy alerting is still running.
<br />
Please contact your administrator to upgrade permanently.
</p>
<TextLink external href={'https://grafana.com/docs/grafana/latest/alerting/set-up/migrating-alerts/'}>
Read about upgrading
</TextLink>
</CollapsibleAlert>
);
};

View File

@@ -90,6 +90,13 @@ const legacyRoutes: RouteDescriptor[] = [
)
),
},
{
path: '/alerting-legacy/upgrade',
roles: () => ['Admin'],
component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingUpgrade" */ 'app/features/alerting/Upgrade')
),
},
];
const unifiedRoutes: RouteDescriptor[] = [
@@ -302,6 +309,10 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
if (cfg.unifiedAlertingEnabled) {
return unifiedRoutes;
} else if (cfg.alertingEnabled) {
if (config.featureToggles.alertingPreviewUpgrade) {
// If preview is enabled, return both legacy and unified routes.
return [...legacyRoutes, ...unifiedRoutes];
}
// Redirect old overlapping legacy routes to new separate ones to minimize unintended 404s.
const redirects = [
{
@@ -330,6 +341,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
return [...legacyRoutes, ...redirects];
}
// Disable all alerting routes.
const uniquePaths = uniq([...legacyRoutes, ...unifiedRoutes].map((route) => route.path));
return uniquePaths.map((path) => ({
path,

View File

@@ -27,6 +27,6 @@ export const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (
export const alertingApi = createApi({
reducerPath: 'alertingApi',
baseQuery: backendSrvBaseQuery(),
tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration', 'OnCallIntegrations'],
tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration', 'OnCallIntegrations', 'OrgMigrationState'],
endpoints: () => ({}),
});

View File

@@ -0,0 +1,444 @@
import { FetchError, isFetchError } from '@grafana/runtime';
import {
createErrorNotification,
createSuccessNotification,
createWarningNotification,
} from '../../../../core/copy/appNotification';
import { notifyApp } from '../../../../core/reducers/appNotification';
import { ObjectMatcher } from '../../../../plugins/datasource/alertmanager/types';
import { alertingApi } from './alertingApi';
export interface OrgMigrationSummary {
newDashboards: number;
newAlerts: number;
newChannels: number;
removed: boolean;
hasErrors: boolean;
}
export interface OrgMigrationState {
orgId: number;
migratedDashboards: DashboardUpgrade[];
migratedChannels: ContactPair[];
errors: string[];
}
export interface DashboardUpgrade {
migratedAlerts: AlertPair[];
dashboardId: number;
dashboardUid: string;
dashboardName: string;
folderUid: string;
folderName: string;
newFolderUid?: string;
newFolderName?: string;
provisioned: boolean;
error?: string;
warning: string;
isUpgrading: boolean;
}
export interface AlertPair {
legacyAlert: LegacyAlert;
alertRule?: AlertRuleUpgrade;
error?: string;
isUpgrading: boolean;
}
export interface ContactPair {
legacyChannel: LegacyChannel;
contactPoint?: ContactPointUpgrade;
provisioned: boolean;
error?: string;
isUpgrading: boolean;
}
export interface LegacyAlert {
id: number;
dashboardId: number;
panelId: number;
name: string;
}
export interface AlertRuleUpgrade {
uid: string;
title: string;
sendsTo: string[];
}
export interface LegacyChannel {
id: number;
name: string;
type: string;
}
export interface ContactPointUpgrade {
name: string;
type: string;
routeMatchers: ObjectMatcher[];
}
function isFetchBaseQueryError(error: unknown): error is { error: FetchError } {
return typeof error === 'object' && error != null && 'error' in error;
}
export const upgradeApi = alertingApi.injectEndpoints({
endpoints: (build) => ({
upgradeChannel: build.mutation<OrgMigrationSummary, { channelId: number; skipExisting: boolean }>({
query: ({ channelId, skipExisting }) => ({
url: `/api/v1/upgrade/channels/${channelId}${skipExisting ? '?skipExisting=true' : ''}`,
method: 'POST',
showSuccessAlert: false,
showErrorAlert: false,
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted({ channelId }, { dispatch, queryFulfilled }) {
try {
dispatch(
upgradeApi.util.updateQueryData('getOrgUpgradeSummary', undefined, (draft) => {
const index = (draft.migratedChannels ?? []).findIndex((pair) => pair.legacyChannel?.id === channelId);
if (index !== -1) {
draft.migratedChannels[index].isUpgrading = true;
}
})
);
const { data } = await queryFulfilled;
if (data.hasErrors) {
dispatch(notifyApp(createWarningNotification(`Failed to upgrade notification channel '${channelId}'`)));
} else {
if (data.removed) {
dispatch(
notifyApp(
createSuccessNotification(
`Notification channel '${channelId}' not found, removed from list of upgrades`
)
)
);
} else {
dispatch(notifyApp(createSuccessNotification(`Upgraded notification channel '${channelId}'`)));
}
}
} catch (e) {
if (isFetchBaseQueryError(e) && isFetchError(e.error)) {
dispatch(notifyApp(createErrorNotification('Request failed', e.error.data.message)));
} else {
dispatch(notifyApp(createErrorNotification(`Request failed`)));
}
}
},
}),
upgradeAllChannels: build.mutation<OrgMigrationSummary, { skipExisting: boolean }>({
query: ({ skipExisting }) => ({
url: `/api/v1/upgrade/channels${skipExisting ? '?skipExisting=true' : ''}`,
method: 'POST',
showSuccessAlert: false,
showErrorAlert: false,
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted({ skipExisting }, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
if (data.hasErrors) {
dispatch(
notifyApp(
createWarningNotification(
`Issues while upgrading ${data.newChannels} ${skipExisting ? 'new ' : ''}notification channels`
)
)
);
} else {
dispatch(
notifyApp(
createSuccessNotification(
`Upgraded ${data.newChannels} ${skipExisting ? 'new ' : ''}notification channels`
)
)
);
}
} catch (e) {
if (isFetchBaseQueryError(e) && isFetchError(e.error)) {
dispatch(notifyApp(createErrorNotification('Request failed', e.error.data.message)));
} else {
dispatch(notifyApp(createErrorNotification(`Request failed`)));
}
}
},
}),
upgradeAlert: build.mutation<OrgMigrationSummary, { dashboardId: number; panelId: number; skipExisting: boolean }>({
query: ({ dashboardId, panelId, skipExisting }) => ({
url: `/api/v1/upgrade/dashboards/${dashboardId}/panels/${panelId}${skipExisting ? '?skipExisting=true' : ''}`,
method: 'POST',
showSuccessAlert: false,
showErrorAlert: false,
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted({ dashboardId, panelId }, { dispatch, queryFulfilled }) {
try {
dispatch(
upgradeApi.util.updateQueryData('getOrgUpgradeSummary', undefined, (draft) => {
const index = (draft.migratedDashboards ?? []).findIndex((du) => du.dashboardId === dashboardId);
if (index !== -1) {
const alertIndex = (draft.migratedDashboards[index]?.migratedAlerts ?? []).findIndex(
(pair) => pair.legacyAlert?.panelId === panelId
);
if (alertIndex !== -1) {
draft.migratedDashboards[index].migratedAlerts[alertIndex].isUpgrading = true;
}
}
})
);
const { data } = await queryFulfilled;
if (data.hasErrors) {
dispatch(
notifyApp(
createWarningNotification(`Failed to upgrade alert from dashboard '${dashboardId}', panel '${panelId}'`)
)
);
} else {
if (data.removed) {
dispatch(
notifyApp(
createSuccessNotification(
`Alert from dashboard '${dashboardId}', panel '${panelId}' not found, removed from list of upgrades`
)
)
);
} else {
dispatch(
notifyApp(
createSuccessNotification(`Upgraded alert from dashboard '${dashboardId}', panel '${panelId}'`)
)
);
}
}
} catch (e) {
if (isFetchBaseQueryError(e) && isFetchError(e.error)) {
dispatch(notifyApp(createErrorNotification('Request failed', e.error.data.message)));
} else {
dispatch(notifyApp(createErrorNotification(`Request failed`)));
}
}
},
}),
upgradeDashboard: build.mutation<OrgMigrationSummary, { dashboardId: number; skipExisting: boolean }>({
query: ({ dashboardId, skipExisting }) => ({
url: `/api/v1/upgrade/dashboards/${dashboardId}${skipExisting ? '?skipExisting=true' : ''}`,
method: 'POST',
showSuccessAlert: false,
showErrorAlert: false,
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted({ dashboardId, skipExisting }, { dispatch, queryFulfilled }) {
try {
dispatch(
upgradeApi.util.updateQueryData('getOrgUpgradeSummary', undefined, (draft) => {
const index = (draft.migratedDashboards ?? []).findIndex((du) => du.dashboardId === dashboardId);
if (index !== -1) {
draft.migratedDashboards[index].isUpgrading = true;
}
})
);
const { data } = await queryFulfilled;
if (data.hasErrors) {
dispatch(
notifyApp(
createWarningNotification(
`Issues while upgrading ${data.newAlerts} ${
skipExisting ? 'new ' : ''
}alerts from dashboard '${dashboardId}'`
)
)
);
} else {
if (data.removed) {
dispatch(
notifyApp(
createSuccessNotification(`Dashboard '${dashboardId}' not found, removed from list of upgrades`)
)
);
} else {
dispatch(
notifyApp(
createSuccessNotification(
`Upgraded ${data.newAlerts} ${skipExisting ? 'new ' : ''}alerts from dashboard '${dashboardId}'`
)
)
);
}
}
} catch (e) {
if (isFetchBaseQueryError(e) && isFetchError(e.error)) {
dispatch(notifyApp(createErrorNotification('Request failed', e.error.data.message)));
} else {
dispatch(notifyApp(createErrorNotification(`Request failed`)));
}
}
},
}),
upgradeAllDashboards: build.mutation<OrgMigrationSummary, { skipExisting: boolean }>({
query: ({ skipExisting }) => ({
url: `/api/v1/upgrade/dashboards${skipExisting ? '?skipExisting=true' : ''}`,
method: 'POST',
showSuccessAlert: false,
showErrorAlert: false,
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted({ skipExisting }, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
if (data.hasErrors) {
dispatch(
notifyApp(
createWarningNotification(
`Issues while upgrading ${data.newAlerts} ${skipExisting ? 'new ' : ''}alerts in ${
data.newDashboards
} dashboards`
)
)
);
} else {
dispatch(
notifyApp(
createSuccessNotification(
`Upgraded ${data.newAlerts} ${skipExisting ? 'new ' : ''}alerts in ${data.newDashboards} dashboards`
)
)
);
}
} catch (e) {
if (isFetchBaseQueryError(e) && isFetchError(e.error)) {
dispatch(notifyApp(createErrorNotification('Request failed', e.error.data.message)));
} else {
dispatch(notifyApp(createErrorNotification(`Request failed`)));
}
}
},
}),
upgradeOrg: build.mutation<OrgMigrationSummary, { skipExisting: boolean }>({
query: ({ skipExisting }) => ({
url: `/api/v1/upgrade/org${skipExisting ? '?skipExisting=true' : ''}`,
method: 'POST',
showSuccessAlert: false,
showErrorAlert: false,
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted({ skipExisting }, { dispatch, getCacheEntry, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
if (data.hasErrors) {
dispatch(
notifyApp(
createWarningNotification(
`Issues while upgrading ${data.newAlerts} ${skipExisting ? 'new ' : ''}alerts in ${
data.newDashboards
} dashboards and ${data.newChannels} ${skipExisting ? 'new ' : ''}notification channels`
)
)
);
} else {
dispatch(
notifyApp(
createSuccessNotification(
`Upgraded ${data.newAlerts} ${skipExisting ? 'new ' : ''}alerts in ${
data.newDashboards
} dashboards and ${data.newChannels} ${skipExisting ? 'new ' : ''}notification channels`
)
)
);
}
} catch (e) {
if (isFetchBaseQueryError(e) && isFetchError(e.error)) {
dispatch(notifyApp(createErrorNotification('Request failed', e.error.data.message)));
} else {
dispatch(notifyApp(createErrorNotification(`Request failed`)));
}
}
},
}),
cancelOrgUpgrade: build.mutation<void, void>({
query: () => ({
url: `/api/v1/upgrade/org`,
method: 'DELETE',
}),
invalidatesTags: ['OrgMigrationState'],
async onQueryStarted(undefined, { dispatch }) {
// This helps prevent flickering of old tables after the cancel button is clicked.
try {
dispatch(
upgradeApi.util.updateQueryData('getOrgUpgradeSummary', undefined, (draft) => {
const defaultState: OrgMigrationState = {
orgId: 0,
migratedDashboards: [],
migratedChannels: [],
errors: [],
};
Object.assign(draft, defaultState);
})
);
} catch {}
},
}),
getOrgUpgradeSummary: build.query<OrgMigrationState, void>({
query: () => ({
url: `/api/v1/upgrade/org`,
}),
providesTags: ['OrgMigrationState'],
transformResponse: (summary: OrgMigrationState): OrgMigrationState => {
summary.migratedDashboards = summary.migratedDashboards ?? [];
summary.migratedChannels = summary.migratedChannels ?? [];
summary.errors = summary.errors ?? [];
// Sort to show the most problematic rows first.
summary.migratedDashboards.forEach((dashUpgrade) => {
// dashUpgrade.isUpgrading = false;
dashUpgrade.migratedAlerts = dashUpgrade.migratedAlerts ?? [];
dashUpgrade.error = dashUpgrade.error ?? '';
dashUpgrade.warning = dashUpgrade.warning ?? '';
dashUpgrade.migratedAlerts.sort((a, b) => {
const byError = (b.error ?? '').localeCompare(a.error ?? '');
if (byError !== 0) {
return byError;
}
return (a.legacyAlert?.name ?? '').localeCompare(b.legacyAlert?.name ?? '');
});
});
summary.migratedDashboards.sort((a, b) => {
const byErrors = (b.error ?? '').localeCompare(a.error ?? '');
if (byErrors !== 0) {
return byErrors;
}
const byNestedErrors =
b.migratedAlerts.filter((a) => a.error).length - a.migratedAlerts.filter((a) => a.error).length;
if (byNestedErrors !== 0) {
return byNestedErrors;
}
const byWarnings = (b.warning ?? '').localeCompare(a.warning ?? '');
if (byWarnings !== 0) {
return byWarnings;
}
const byFolder = a.folderName.localeCompare(b.folderName);
if (byFolder !== 0) {
return byFolder;
}
return a.dashboardName.localeCompare(b.dashboardName);
});
// Sort contacts.
summary.migratedChannels.sort((a, b) => {
const byErrors = (b.error ? 1 : 0) - (a.error ? 1 : 0);
if (byErrors !== 0) {
return byErrors;
}
return (a.legacyChannel?.name ?? '').localeCompare(b.legacyChannel?.name ?? '');
});
return summary;
},
}),
}),
});

View File

@@ -4,6 +4,7 @@ import { useLocation } from 'react-use';
import { Page } from 'app/core/components/Page/Page';
import { PageProps } from 'app/core/components/Page/types';
import { UAPreviewNotice } from '../../components/UAPreviewNotice';
import { AlertmanagerProvider, useAlertmanager } from '../state/AlertmanagerContext';
import { AlertManagerPicker } from './AlertManagerPicker';
@@ -18,7 +19,12 @@ interface AlertingPageWrapperProps extends PageProps {
export const AlertingPageWrapper = ({ children, isLoading, ...rest }: AlertingPageWrapperProps) => (
<Page {...rest}>
<Page.Contents isLoading={isLoading}>{children}</Page.Contents>
<Page.Contents isLoading={isLoading}>
<div>
<UAPreviewNotice />
{children}
</div>
</Page.Contents>
</Page>
);

View File

@@ -710,6 +710,10 @@
"subtitle": "Schalte Benachrichtigungen von einer oder mehrerer Warnregeln aus",
"title": "Stummschalten"
},
"alerting-upgrade": {
"subtitle": "",
"title": ""
},
"alerts-and-incidents": {
"subtitle": "Warn- und Vorfallmanagement-Apps",
"title": "Warnungen und IRM"

View File

@@ -710,6 +710,10 @@
"subtitle": "Stop notifications from one or more alerting rules",
"title": "Silences"
},
"alerting-upgrade": {
"subtitle": "Upgrade your existing legacy alerts and notification channels to the new Grafana Alerting",
"title": "Alerting upgrade"
},
"alerts-and-incidents": {
"subtitle": "Alerting and incident management apps",
"title": "Alerts & IRM"

View File

@@ -716,6 +716,10 @@
"subtitle": "Detener notificaciones de una o más reglas de alerta",
"title": "Silencios"
},
"alerting-upgrade": {
"subtitle": "",
"title": ""
},
"alerts-and-incidents": {
"subtitle": "Aplicaciones de gestión de alertas e incidentes",
"title": "Alertas e IRM"

View File

@@ -716,6 +716,10 @@
"subtitle": "Arrêter les notifications résultant d'une ou plusieurs règles d'alerte",
"title": "Silences"
},
"alerting-upgrade": {
"subtitle": "",
"title": ""
},
"alerts-and-incidents": {
"subtitle": "Applications de gestion des incidents et des alertes",
"title": "Alertes et IRM"

View File

@@ -710,6 +710,10 @@
"subtitle": "Ŝŧőp ʼnőŧįƒįčäŧįőʼnş ƒřőm őʼnę őř mőřę äľęřŧįʼnģ řūľęş",
"title": "Ŝįľęʼnčęş"
},
"alerting-upgrade": {
"subtitle": "Ůpģřäđę yőūř ęχįşŧįʼnģ ľęģäčy äľęřŧş äʼnđ ʼnőŧįƒįčäŧįőʼn čĥäʼnʼnęľş ŧő ŧĥę ʼnęŵ Ğřäƒäʼnä Åľęřŧįʼnģ",
"title": "Åľęřŧįʼnģ ūpģřäđę"
},
"alerts-and-incidents": {
"subtitle": "Åľęřŧįʼnģ äʼnđ įʼnčįđęʼnŧ mäʼnäģęmęʼnŧ äppş",
"title": "Åľęřŧş & ĨŖM"

View File

@@ -704,6 +704,10 @@
"subtitle": "停止来自一个或多个警报规则的通知",
"title": "静默"
},
"alerting-upgrade": {
"subtitle": "",
"title": ""
},
"alerts-and-incidents": {
"subtitle": "警报和事件管理应用",
"title": "警报和 IRM"

View File

@@ -2569,6 +2569,20 @@
},
"type": "object"
},
"AlertPair": {
"properties": {
"alertRule": {
"$ref": "#/components/schemas/AlertRuleUpgrade"
},
"error": {
"type": "string"
},
"legacyAlert": {
"$ref": "#/components/schemas/LegacyAlert"
}
},
"type": "object"
},
"AlertQuery": {
"properties": {
"datasourceUid": {
@@ -2753,6 +2767,23 @@
},
"type": "object"
},
"AlertRuleUpgrade": {
"properties": {
"sendsTo": {
"items": {
"type": "string"
},
"type": "array"
},
"title": {
"type": "string"
},
"uid": {
"type": "string"
}
},
"type": "object"
},
"AlertStateInfoDTO": {
"properties": {
"dashboardId": {
@@ -3667,6 +3698,20 @@
"title": "Config is the top-level configuration for Alertmanager's config files.",
"type": "object"
},
"ContactPair": {
"properties": {
"contactPoint": {
"$ref": "#/components/schemas/ContactPointUpgrade"
},
"error": {
"type": "string"
},
"legacyChannel": {
"$ref": "#/components/schemas/LegacyChannel"
}
},
"type": "object"
},
"ContactPointExport": {
"properties": {
"name": {
@@ -3686,6 +3731,20 @@
"title": "ContactPointExport is the provisioned file export of alerting.ContactPointV1.",
"type": "object"
},
"ContactPointUpgrade": {
"properties": {
"name": {
"type": "string"
},
"routeMatchers": {
"$ref": "#/components/schemas/ObjectMatchers"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"ContactPoints": {
"items": {
"$ref": "#/components/schemas/EmbeddedContactPoint"
@@ -4430,6 +4489,48 @@
},
"type": "object"
},
"DashboardUpgrade": {
"properties": {
"dashboardId": {
"format": "int64",
"type": "integer"
},
"dashboardName": {
"type": "string"
},
"dashboardUid": {
"type": "string"
},
"error": {
"type": "string"
},
"folderName": {
"type": "string"
},
"folderUid": {
"type": "string"
},
"migratedAlerts": {
"items": {
"$ref": "#/components/schemas/AlertPair"
},
"type": "array"
},
"newFolderName": {
"type": "string"
},
"newFolderUid": {
"type": "string"
},
"provisioned": {
"type": "boolean"
},
"warning": {
"type": "string"
}
},
"type": "object"
},
"DashboardVersionMeta": {
"description": "DashboardVersionMeta extends the DashboardVersionDTO with the names\nassociated with the UserIds, overriding the field with the same name from\nthe DashboardVersionDTO model.",
"properties": {
@@ -6438,6 +6539,21 @@
},
"type": "object"
},
"LegacyChannel": {
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
}
},
"type": "object"
},
"LibraryElementArrayResponse": {
"properties": {
"result": {
@@ -7214,6 +7330,50 @@
},
"type": "object"
},
"OrgMigrationState": {
"properties": {
"migratedChannels": {
"items": {
"$ref": "#/components/schemas/ContactPair"
},
"type": "array"
},
"migratedDashboards": {
"items": {
"$ref": "#/components/schemas/DashboardUpgrade"
},
"type": "array"
},
"orgId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"OrgMigrationSummary": {
"properties": {
"hasErrors": {
"type": "boolean"
},
"newAlerts": {
"format": "int64",
"type": "integer"
},
"newChannels": {
"format": "int64",
"type": "integer"
},
"newDashboards": {
"format": "int64",
"type": "integer"
},
"removed": {
"type": "boolean"
}
},
"type": "object"
},
"OrgUserDTO": {
"properties": {
"accessControl": {
@@ -11762,6 +11922,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/components/schemas/alertGroup"
},
@@ -12010,6 +12171,7 @@
"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",
@@ -12153,7 +12315,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@@ -12219,7 +12380,6 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",