Files
grafana/pkg/registry/apps/alerting/rules/alertrule/compat.go
T
Moustafa Baiou cb7abbaa0f Alerting: Rename expression elements of Rules APIs (#110914)
This renames `data` to `expressions` for clarity in the rules apis.

Also makes certain fields that are redundant optional in the case of pure expressions, so that users don't have to specify them when they are not needed (e.g. not datasource queries).
2025-09-12 22:15:55 +00:00

387 lines
13 KiB
Go

package alertrule
import (
"encoding/json"
"fmt"
"slices"
"strconv"
"time"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
prom_model "github.com/prometheus/common/model"
)
var (
errInvalidRule = fmt.Errorf("rule is not a alerting rule")
)
func convertToK8sResource(
orgID int64,
rule *ngmodels.AlertRule,
provenance ngmodels.Provenance,
namespaceMapper request.NamespaceMapper,
) (*model.AlertRule, error) {
if rule.Type() != ngmodels.RuleTypeAlerting {
return nil, errInvalidRule
}
interval, err := prom_model.ParseDuration(fmt.Sprintf("%ds", rule.IntervalSeconds))
if err != nil {
return nil, fmt.Errorf("failed to parse interval: %w", err)
}
k8sRule := &model.AlertRule{
ObjectMeta: metav1.ObjectMeta{
Name: rule.UID,
Namespace: namespaceMapper(orgID),
ResourceVersion: fmt.Sprint(rule.Version),
Labels: make(map[string]string),
},
Spec: model.AlertRuleSpec{
Title: rule.Title,
Expressions: make(model.AlertRuleExpressionMap),
Trigger: model.AlertRuleIntervalTrigger{
Interval: model.AlertRulePromDuration(interval.String()),
},
Labels: make(map[string]model.AlertRuleTemplateString),
Annotations: make(map[string]model.AlertRuleTemplateString),
NoDataState: string(rule.NoDataState),
ExecErrState: string(rule.ExecErrState),
MissingSeriesEvalsToResolve: rule.MissingSeriesEvalsToResolve,
},
}
if rule.IsPaused {
k8sRule.Spec.Paused = util.Pointer(true)
}
if rule.RuleGroup != "" && !ngmodels.IsNoGroupRuleGroup(rule.RuleGroup) {
k8sRule.Labels[model.GroupLabelKey] = rule.RuleGroup
k8sRule.Labels[model.GroupIndexLabelKey] = strconv.Itoa(rule.RuleGroupIndex)
}
if rule.For != 0 {
k8sRule.Spec.For = util.Pointer(rule.For.String())
}
if rule.KeepFiringFor != 0 {
k8sRule.Spec.KeepFiringFor = util.Pointer(rule.KeepFiringFor.String())
}
if rule.PanelID != nil && rule.DashboardUID != nil &&
*rule.PanelID > 0 && *rule.DashboardUID != "" {
k8sRule.Spec.PanelRef = &model.AlertRuleV0alpha1SpecPanelRef{
PanelID: *rule.PanelID,
DashboardUID: *rule.DashboardUID,
}
}
for k, v := range rule.Annotations {
k8sRule.Spec.Annotations[k] = model.AlertRuleTemplateString(v)
}
for k, v := range rule.Labels {
k8sRule.Spec.Labels[k] = model.AlertRuleTemplateString(v)
}
for _, query := range rule.Data {
k8sRule.Spec.Expressions[query.RefID] = convertToK8sExpression(query, rule)
}
for _, setting := range rule.NotificationSettings {
nfSetting := model.AlertRuleV0alpha1SpecNotificationSettings{
Receiver: setting.Receiver,
GroupBy: setting.GroupBy,
}
if setting.GroupWait != nil {
nfSetting.GroupWait = util.Pointer(model.AlertRulePromDuration(setting.GroupWait.String()))
}
if setting.GroupInterval != nil {
nfSetting.GroupInterval = util.Pointer(model.AlertRulePromDuration(setting.GroupInterval.String()))
}
if setting.RepeatInterval != nil {
nfSetting.RepeatInterval = util.Pointer(model.AlertRulePromDuration(setting.RepeatInterval.String()))
}
if setting.MuteTimeIntervals != nil {
nfSetting.MuteTimeIntervals = make([]model.AlertRuleTimeIntervalRef, 0, len(setting.MuteTimeIntervals))
for _, m := range setting.MuteTimeIntervals {
nfSetting.MuteTimeIntervals = append(nfSetting.MuteTimeIntervals, model.AlertRuleTimeIntervalRef(m))
}
}
if setting.ActiveTimeIntervals != nil {
nfSetting.ActiveTimeIntervals = make([]model.AlertRuleTimeIntervalRef, 0, len(setting.ActiveTimeIntervals))
for _, a := range setting.ActiveTimeIntervals {
nfSetting.ActiveTimeIntervals = append(nfSetting.ActiveTimeIntervals, model.AlertRuleTimeIntervalRef(a))
}
}
k8sRule.Spec.NotificationSettings = &nfSetting
}
meta, err := utils.MetaAccessor(k8sRule)
if err != nil {
return nil, fmt.Errorf("failed to get metadata: %w", err)
}
meta.SetFolder(rule.NamespaceUID)
if rule.UpdatedBy != nil {
meta.SetUpdatedBy(string(*rule.UpdatedBy))
k8sRule.SetUpdatedBy(string(*rule.UpdatedBy))
}
meta.SetUpdatedTimestamp(&rule.Updated)
k8sRule.SetUpdateTimestamp(rule.Updated)
if err := k8sRule.SetProvenanceStatus(string(provenance)); err != nil {
return nil, fmt.Errorf("failed to set provenance status: %w", err)
}
// FIXME: we don't have a creation timestamp in the domain model, so we can't set it here.
// We should consider adding it to the domain model. Migration can set it to the Updated timestamp for existing
// k8sRule.SetCreationTimestamp(rule.)
k8sRule.UID = gapiutil.CalculateClusterWideUID(k8sRule)
return k8sRule, nil
}
func convertToK8sExpression(query ngmodels.AlertQuery, rule *ngmodels.AlertRule) model.AlertRuleExpression {
expression := model.AlertRuleExpression{
Model: query.Model,
}
if query.QueryType != "" {
expression.QueryType = util.Pointer(query.QueryType)
}
// DatasourceUID is optional and defaults to expr datasource
if !expr.IsDataSource(query.DatasourceUID) {
expression.DatasourceUID = util.Pointer(model.AlertRuleDatasourceUID(query.DatasourceUID))
}
if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 {
expression.RelativeTimeRange = &model.AlertRuleRelativeTimeRange{
From: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.From.String()),
To: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.To.String()),
}
}
if rule.Condition == query.RefID {
expression.Source = util.Pointer(true)
}
return expression
}
func convertToK8sResources(
orgID int64,
rules []*ngmodels.AlertRule,
provenanceMap map[string]ngmodels.Provenance,
namespaceMapper request.NamespaceMapper,
continueToken string,
) (*model.AlertRuleList, error) {
k8sRules := &model.AlertRuleList{
ListMeta: metav1.ListMeta{
Continue: continueToken,
},
Items: make([]model.AlertRule, 0, len(rules)),
}
for _, rule := range rules {
provenance := provenanceMap[rule.UID]
k8sRule, err := convertToK8sResource(orgID, rule, provenance, namespaceMapper)
if err != nil {
return nil, fmt.Errorf("failed to convert to k8s resource: %w", err)
}
k8sRules.Items = append(k8sRules.Items, *k8sRule)
}
return k8sRules, nil
}
func convertToDomainModel(orgID int64, k8sRule *model.AlertRule) (*ngmodels.AlertRule, ngmodels.Provenance, error) {
domainRule, err := convertToBaseDomainModel(orgID, k8sRule)
if err != nil {
return nil, ngmodels.ProvenanceNone, fmt.Errorf("failed to convert to domain model: %w", err)
}
sourceProv := k8sRule.GetProvenanceStatus()
if !slices.Contains(model.AcceptedProvenanceStatuses, sourceProv) {
return nil, ngmodels.ProvenanceNone, fmt.Errorf("invalid provenance status: %s", sourceProv)
}
provenance := ngmodels.Provenance(sourceProv)
return domainRule, provenance, nil
}
func convertToBaseDomainModel(orgID int64, k8sRule *model.AlertRule) (*ngmodels.AlertRule, error) {
domainRule := &ngmodels.AlertRule{
OrgID: orgID,
UID: k8sRule.Name,
Title: k8sRule.Spec.Title,
NamespaceUID: k8sRule.Namespace,
Data: make([]ngmodels.AlertQuery, 0, len(k8sRule.Spec.Expressions)),
IsPaused: k8sRule.Spec.Paused != nil && *k8sRule.Spec.Paused,
Labels: make(map[string]string),
Annotations: make(map[string]string),
NoDataState: ngmodels.NoDataState(k8sRule.Spec.NoDataStateOrDefault()),
ExecErrState: ngmodels.ExecutionErrorState(k8sRule.Spec.ExecErrStateOrDefault()),
}
meta, err := utils.MetaAccessor(k8sRule)
if err != nil {
return nil, fmt.Errorf("failed to get metadata: %w", err)
}
if group, ok := k8sRule.Labels[model.GroupLabelKey]; ok {
domainRule.RuleGroup = group
}
if groupIndexStr, ok := k8sRule.Labels[model.GroupIndexLabelKey]; ok {
groupIndex, err := strconv.Atoi(groupIndexStr)
if err != nil {
return nil, fmt.Errorf("failed to parse group index: %w", err)
}
domainRule.RuleGroupIndex = groupIndex
}
domainRule.NamespaceUID = meta.GetFolder()
for k, v := range k8sRule.Spec.Annotations {
domainRule.Annotations[k] = string(v)
}
for k, v := range k8sRule.Spec.Labels {
domainRule.Labels[k] = string(v)
}
if k8sRule.Spec.PanelRef != nil {
domainRule.PanelID = &k8sRule.Spec.PanelRef.PanelID
domainRule.DashboardUID = &k8sRule.Spec.PanelRef.DashboardUID
}
if k8sRule.Spec.MissingSeriesEvalsToResolve != nil {
src := *k8sRule.Spec.MissingSeriesEvalsToResolve
domainRule.MissingSeriesEvalsToResolve = &src
}
if k8sRule.Spec.For != nil {
pendingPeriod, err := prom_model.ParseDuration(*k8sRule.Spec.For)
if err != nil {
return nil, fmt.Errorf("failed to parse duration: %w", err)
}
domainRule.For = time.Duration(pendingPeriod)
}
if k8sRule.Spec.KeepFiringFor != nil {
keepFiringFor, err := prom_model.ParseDuration(*k8sRule.Spec.KeepFiringFor)
if err != nil {
return nil, fmt.Errorf("failed to parse duration: %w", err)
}
domainRule.KeepFiringFor = time.Duration(keepFiringFor)
}
interval, err := prom_model.ParseDuration(string(k8sRule.Spec.Trigger.Interval))
if err != nil {
return nil, fmt.Errorf("failed to parse interval: %w", err)
}
domainRule.IntervalSeconds = int64(time.Duration(interval).Seconds())
for refID, expression := range k8sRule.Spec.Expressions {
domainQuery, err := convertToDomainQuery(expression, refID)
if err != nil {
return nil, err
}
domainRule.Data = append(domainRule.Data, domainQuery)
if expression.Source != nil && *expression.Source {
if domainRule.Condition != "" {
return nil, fmt.Errorf("multiple queries marked as source: %s and %s", domainRule.Condition, refID)
}
domainRule.Condition = refID
}
}
if domainRule.Condition == "" {
return nil, fmt.Errorf("no query marked as source")
}
sourceSettings := k8sRule.Spec.NotificationSettings
if sourceSettings != nil {
settings, err := convertNotificationSettings(sourceSettings)
if err != nil {
return nil, err
}
domainRule.NotificationSettings = []ngmodels.NotificationSettings{settings}
}
return domainRule, nil
}
func convertNotificationSettings(sourceSettings *model.AlertRuleV0alpha1SpecNotificationSettings) (ngmodels.NotificationSettings, error) {
settings := ngmodels.NotificationSettings{
Receiver: sourceSettings.Receiver,
GroupBy: sourceSettings.GroupBy,
}
if sourceSettings.GroupWait != nil {
groupWait, err := prom_model.ParseDuration(string(*sourceSettings.GroupWait))
if err != nil {
return ngmodels.NotificationSettings{}, fmt.Errorf("failed to parse duration: %w", err)
}
settings.GroupWait = &groupWait
}
if sourceSettings.GroupInterval != nil {
groupInterval, err := prom_model.ParseDuration(string(*sourceSettings.GroupInterval))
if err != nil {
return ngmodels.NotificationSettings{}, fmt.Errorf("failed to parse duration: %w", err)
}
settings.GroupInterval = &groupInterval
}
if sourceSettings.RepeatInterval != nil {
repeatInterval, err := prom_model.ParseDuration(string(*sourceSettings.RepeatInterval))
if err != nil {
return ngmodels.NotificationSettings{}, fmt.Errorf("failed to parse duration: %w", err)
}
settings.RepeatInterval = &repeatInterval
}
if sourceSettings.MuteTimeIntervals != nil {
settings.MuteTimeIntervals = make([]string, 0, len(sourceSettings.MuteTimeIntervals))
for _, m := range sourceSettings.MuteTimeIntervals {
muteInterval := string(m)
settings.MuteTimeIntervals = append(settings.MuteTimeIntervals, muteInterval)
}
}
if sourceSettings.ActiveTimeIntervals != nil {
settings.ActiveTimeIntervals = make([]string, 0, len(sourceSettings.ActiveTimeIntervals))
for _, a := range sourceSettings.ActiveTimeIntervals {
activeTimeInterval := string(a)
settings.ActiveTimeIntervals = append(settings.ActiveTimeIntervals, activeTimeInterval)
}
}
return settings, nil
}
func convertToDomainQuery(expression model.AlertRuleExpression, refID string) (ngmodels.AlertQuery, error) {
modelJson, err := json.Marshal(expression.Model)
if err != nil {
return ngmodels.AlertQuery{}, fmt.Errorf("failed to marshal model: %w", err)
}
domainQuery := ngmodels.AlertQuery{
RefID: refID,
Model: modelJson,
}
if expression.QueryType != nil {
domainQuery.QueryType = *expression.QueryType
}
if expression.DatasourceUID != nil {
domainQuery.DatasourceUID = string(*expression.DatasourceUID)
} else {
domainQuery.DatasourceUID = expr.DatasourceUID
}
if expression.RelativeTimeRange != nil {
from, err := prom_model.ParseDuration(string(expression.RelativeTimeRange.From))
if err != nil {
return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err)
}
to, err := prom_model.ParseDuration(string(expression.RelativeTimeRange.To))
if err != nil {
return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err)
}
domainQuery.RelativeTimeRange = ngmodels.RelativeTimeRange{
From: ngmodels.Duration(from),
To: ngmodels.Duration(to),
}
}
return domainQuery, nil
}