mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
140
pkg/services/ngalert/api/api_upgrade.go
Normal file
140
pkg/services/ngalert/api/api_upgrade.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
163
pkg/services/ngalert/api/generated_base_api_upgrade.go
Normal file
163
pkg/services/ngalert/api/generated_base_api_upgrade.go
Normal 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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
189
pkg/services/ngalert/api/tooling/definitions/upgrade.go
Normal file
189
pkg/services/ngalert/api/tooling/definitions/upgrade.go
Normal 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"`
|
||||
}
|
||||
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
48
pkg/services/ngalert/api/upgrade.go
Normal file
48
pkg/services/ngalert/api/upgrade.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
26
pkg/services/ngalert/migration/store/slim_models.go
Normal file
26
pkg/services/ngalert/migration/store/slim_models.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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':
|
||||
|
||||
1661
public/app/features/alerting/Upgrade.tsx
Normal file
1661
public/app/features/alerting/Upgrade.tsx
Normal file
File diff suppressed because it is too large
Load Diff
73
public/app/features/alerting/components/CollapsibleAlert.tsx
Normal file
73
public/app/features/alerting/components/CollapsibleAlert.tsx
Normal 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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
33
public/app/features/alerting/components/UAPreviewNotice.tsx
Normal file
33
public/app/features/alerting/components/UAPreviewNotice.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => ({}),
|
||||
});
|
||||
|
||||
444
public/app/features/alerting/unified/api/upgradeApi.ts
Normal file
444
public/app/features/alerting/unified/api/upgradeApi.ts
Normal 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;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -704,6 +704,10 @@
|
||||
"subtitle": "停止来自一个或多个警报规则的通知",
|
||||
"title": "静默"
|
||||
},
|
||||
"alerting-upgrade": {
|
||||
"subtitle": "",
|
||||
"title": ""
|
||||
},
|
||||
"alerts-and-incidents": {
|
||||
"subtitle": "警报和事件管理应用",
|
||||
"title": "警报和 IRM"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user