Alerting: Add support for alpha rules apis in legacy storage
Rules created in the new api makes the rule have no group in the database, but the rule is returned in the old group api with a sentinel group name formatted with the rule uid for compatiblity with the old api. This makes the UI continue to work with the rules without a group, and the ruler will continue to work with the rules without a group. Rules are not allowed to be created in the provisioning api with a NoGroup sentinel mask, but NoGroup rules can be manipulated through both the new and old apis. Co-authored-by: William Wernert <william.wernert@grafana.com>
This commit is contained in:
committed by
Moustafa Baiou
co-authored by
William Wernert
parent
0a85a30642
commit
ca8324e62a
@@ -0,0 +1,50 @@
|
||||
package alertrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if attr.GetResource() != ResourceInfo.GroupResource().Resource {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "valid user is required", err
|
||||
}
|
||||
|
||||
var action accesscontrol.Evaluator
|
||||
// Default evaluator - all actions require read permissions
|
||||
defaultEvaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleRead)
|
||||
|
||||
switch attr.GetVerb() {
|
||||
case "get", "list", "watch":
|
||||
action = defaultEvaluator
|
||||
case "create":
|
||||
action = accesscontrol.EvalAll(
|
||||
defaultEvaluator,
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleCreate),
|
||||
)
|
||||
case "patch", "update":
|
||||
action = accesscontrol.EvalAll(
|
||||
defaultEvaluator,
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleUpdate),
|
||||
)
|
||||
case "delete", "deletecollection":
|
||||
action = accesscontrol.EvalAll(
|
||||
defaultEvaluator,
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete),
|
||||
)
|
||||
}
|
||||
|
||||
ok, err := ac.Evaluate(ctx, user, action)
|
||||
if ok {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package alertrule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"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,
|
||||
Data: make(map[string]model.AlertRuleQuery),
|
||||
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 {
|
||||
k8sQuery := model.AlertRuleQuery{
|
||||
QueryType: query.QueryType,
|
||||
Model: query.Model,
|
||||
DatasourceUID: model.AlertRuleDatasourceUID(query.DatasourceUID),
|
||||
Source: util.Pointer(rule.Condition == query.RefID),
|
||||
}
|
||||
if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 {
|
||||
k8sQuery.RelativeTimeRange = &model.AlertRuleRelativeTimeRange{
|
||||
From: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.From.String()),
|
||||
To: model.AlertRulePromDurationWMillis(query.RelativeTimeRange.To.String()),
|
||||
}
|
||||
}
|
||||
k8sRule.Spec.Data[query.RefID] = k8sQuery
|
||||
}
|
||||
|
||||
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 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.Data)),
|
||||
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, query := range k8sRule.Spec.Data {
|
||||
domainQuery, err := convertToDomainQuery(query, refID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainRule.Data = append(domainRule.Data, domainQuery)
|
||||
if query.Source != nil && *query.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(query model.AlertRuleQuery, refID string) (ngmodels.AlertQuery, error) {
|
||||
modelJson, err := json.Marshal(query.Model)
|
||||
if err != nil {
|
||||
return ngmodels.AlertQuery{}, fmt.Errorf("failed to marshal model: %w", err)
|
||||
}
|
||||
domainQuery := ngmodels.AlertQuery{
|
||||
RefID: refID,
|
||||
QueryType: query.QueryType,
|
||||
DatasourceUID: string(query.DatasourceUID),
|
||||
Model: modelJson,
|
||||
}
|
||||
if query.RelativeTimeRange != nil {
|
||||
from, err := prom_model.ParseDuration(string(query.RelativeTimeRange.From))
|
||||
if err != nil {
|
||||
return ngmodels.AlertQuery{}, fmt.Errorf("failed to parse duration: %w", err)
|
||||
}
|
||||
to, err := prom_model.ParseDuration(string(query.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
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package alertrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
)
|
||||
|
||||
var (
|
||||
_ grafanarest.Storage = (*legacyStorage)(nil)
|
||||
)
|
||||
|
||||
type legacyStorage struct {
|
||||
service provisioning.AlertRuleService
|
||||
namespacer request.NamespaceMapper
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
func (s *legacyStorage) New() runtime.Object {
|
||||
return ResourceInfo.NewFunc()
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Destroy() {}
|
||||
|
||||
func (s *legacyStorage) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *legacyStorage) GetSingularName() string {
|
||||
return ResourceInfo.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *legacyStorage) NewList() runtime.Object {
|
||||
return ResourceInfo.NewListFunc()
|
||||
}
|
||||
|
||||
func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, provenanceMap, continueToken, err := s.service.ListAlertRules(ctx, user, provisioning.ListAlertRulesOptions{
|
||||
RuleType: ngmodels.RuleTypeFilterAlerting,
|
||||
Limit: opts.Limit,
|
||||
ContinueToken: opts.Continue,
|
||||
// TODO: add field selectors for filtering
|
||||
// TODO: add label selectors for filtering on group and folders
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertToK8sResources(info.OrgID, rules, provenanceMap, s.namespacer, continueToken)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rule, provenance, err := s.service.GetAlertRule(ctx, user, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
|
||||
return nil, k8serrors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, err := convertToK8sResource(info.OrgID, &rule, provenance, s.namespacer)
|
||||
if err != nil && errors.Is(err, errInvalidRule) {
|
||||
return nil, k8serrors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if createValidation != nil {
|
||||
if err := createValidation(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
p, ok := obj.(*model.AlertRule)
|
||||
if !ok {
|
||||
return nil, k8serrors.NewBadRequest("expected valid alert rule object")
|
||||
}
|
||||
|
||||
if p.GenerateName != "" {
|
||||
return nil, fmt.Errorf("generate-name is not supported in legacy storage mode")
|
||||
}
|
||||
// TODO: move this to the validation function
|
||||
if p.Labels[model.GroupLabelKey] != "" || p.Labels[model.GroupIndexLabelKey] != "" {
|
||||
return nil, k8serrors.NewBadRequest("cannot set group when creating alert rule")
|
||||
}
|
||||
|
||||
model, provenance, err := convertToDomainModel(info.OrgID, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created, err := s.service.CreateAlertRule(ctx, user, *model, provenance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertToK8sResource(info.OrgID, &created, provenance, s.namespacer)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
old, err := s.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
|
||||
current, ok := old.(*model.AlertRule)
|
||||
if !ok {
|
||||
// this shouldn't really be possible
|
||||
return nil, false, k8serrors.NewBadRequest("expected valid alert rule object")
|
||||
}
|
||||
|
||||
obj, err := objInfo.UpdatedObject(ctx, old)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
if updateValidation != nil {
|
||||
if err := updateValidation(ctx, obj, old); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
new, ok := obj.(*model.AlertRule)
|
||||
if !ok {
|
||||
return nil, false, k8serrors.NewBadRequest("expected valid alert rule object")
|
||||
}
|
||||
if current.Labels[model.GroupLabelKey] == "" && new.Labels[model.GroupLabelKey] != "" {
|
||||
return nil, false, k8serrors.NewBadRequest("cannot set group label when updating un-grouped alert rule")
|
||||
}
|
||||
|
||||
model, provenance, err := convertToDomainModel(info.OrgID, new)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
|
||||
// ignore returned rule as it doesn't contain the updated version
|
||||
_, err = s.service.UpdateAlertRule(ctx, user, *model, provenance)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
updated, provenance, err := s.service.GetAlertRule(ctx, user, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
rule, err := convertToK8sResource(info.OrgID, &updated, provenance, s.namespacer)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return rule, false, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, opts *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
old, err := s.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
if deleteValidation != nil {
|
||||
if err := deleteValidation(ctx, old); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
p, ok := old.(*model.AlertRule)
|
||||
if !ok {
|
||||
return nil, false, k8serrors.NewBadRequest("expected valid recording rule object")
|
||||
}
|
||||
|
||||
sourceProv := p.GetProvenanceStatus()
|
||||
if !slices.Contains(model.AcceptedProvenanceStatuses, sourceProv) {
|
||||
return nil, false, fmt.Errorf("invalid provenance status: %s", sourceProv)
|
||||
}
|
||||
provenance := ngmodels.Provenance(sourceProv)
|
||||
|
||||
err = s.service.DeleteAlertRule(ctx, user, name, provenance)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
|
||||
return old, false, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) DeleteCollection(ctx context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *internalversion.ListOptions) (runtime.Object, error) {
|
||||
// TODO: support this once a pattern is established for bulk delete operations
|
||||
return nil, k8serrors.NewMethodNotSupported(ResourceInfo.GroupResource(), "delete")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package alertrule
|
||||
|
||||
import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
)
|
||||
|
||||
func NewStorage(legacySvc provisioning.AlertRuleService, namespacer request.NamespaceMapper) grafanarest.Storage {
|
||||
return &legacyStorage{
|
||||
service: legacySvc,
|
||||
namespacer: namespacer,
|
||||
tableConverter: rest.NewDefaultTableConvertor(ResourceInfo.GroupResource()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package alertrule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
var kind = model.AlertRuleKind()
|
||||
|
||||
var ResourceInfo = utils.NewResourceInfo(kind.Group(), kind.Version(),
|
||||
kind.GroupVersionResource().Resource, strings.ToLower(kind.Kind()), kind.Kind(),
|
||||
func() runtime.Object { return kind.ZeroValue() },
|
||||
func() runtime.Object { return kind.ZeroListValue() },
|
||||
utils.TableColumns{},
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
package recordingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if attr.GetResource() != ResourceInfo.GroupResource().Resource {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "valid user is required", err
|
||||
}
|
||||
|
||||
var action accesscontrol.Evaluator
|
||||
// Default evaluator - all actions require read permissions
|
||||
defaultEvaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleRead)
|
||||
|
||||
switch attr.GetVerb() {
|
||||
case "get", "list", "watch":
|
||||
action = defaultEvaluator
|
||||
case "create":
|
||||
action = accesscontrol.EvalAll(
|
||||
defaultEvaluator,
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleCreate),
|
||||
)
|
||||
case "patch", "update":
|
||||
action = accesscontrol.EvalAll(
|
||||
defaultEvaluator,
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleUpdate),
|
||||
)
|
||||
case "delete", "deletecollection":
|
||||
action = accesscontrol.EvalAll(
|
||||
defaultEvaluator,
|
||||
accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete),
|
||||
)
|
||||
}
|
||||
|
||||
ok, err := ac.Evaluate(ctx, user, action)
|
||||
if ok {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package recordingrule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
prom_model "github.com/prometheus/common/model"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidRule = fmt.Errorf("rule is not a recording rule")
|
||||
)
|
||||
|
||||
func convertToK8sResource(
|
||||
orgID int64,
|
||||
rule *ngmodels.AlertRule,
|
||||
provenance ngmodels.Provenance,
|
||||
namespaceMapper request.NamespaceMapper,
|
||||
) (*model.RecordingRule, error) {
|
||||
if rule.Type() != ngmodels.RuleTypeRecording {
|
||||
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.RecordingRule{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: rule.UID,
|
||||
Namespace: namespaceMapper(orgID),
|
||||
ResourceVersion: fmt.Sprint(rule.Version),
|
||||
Labels: make(map[string]string),
|
||||
},
|
||||
Spec: model.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Data: make(map[string]model.RecordingRuleQuery),
|
||||
Trigger: model.RecordingRuleIntervalTrigger{
|
||||
Interval: model.RecordingRulePromDuration(interval.String()),
|
||||
},
|
||||
Labels: make(map[string]model.RecordingRuleTemplateString),
|
||||
Metric: rule.Record.Metric,
|
||||
TargetDatasourceUID: rule.Record.TargetDatasourceUID,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
for k, v := range rule.Labels {
|
||||
k8sRule.Spec.Labels[k] = model.RecordingRuleTemplateString(v)
|
||||
}
|
||||
|
||||
for _, query := range rule.Data {
|
||||
k8sQuery := model.RecordingRuleQuery{
|
||||
QueryType: query.QueryType,
|
||||
Model: query.Model,
|
||||
DatasourceUID: model.RecordingRuleDatasourceUID(query.DatasourceUID),
|
||||
}
|
||||
if time.Duration(query.RelativeTimeRange.From) > 0 || time.Duration(query.RelativeTimeRange.To) > 0 {
|
||||
k8sQuery.RelativeTimeRange = &model.RecordingRuleRelativeTimeRange{
|
||||
From: model.RecordingRulePromDurationWMillis(query.RelativeTimeRange.From.String()),
|
||||
To: model.RecordingRulePromDurationWMillis(query.RelativeTimeRange.To.String()),
|
||||
}
|
||||
}
|
||||
if rule.Record != nil && rule.Record.From == query.RefID {
|
||||
k8sQuery.Source = util.Pointer(true)
|
||||
}
|
||||
k8sRule.Spec.Data[query.RefID] = k8sQuery
|
||||
}
|
||||
|
||||
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 convertToK8sResources(
|
||||
orgID int64,
|
||||
rules []*ngmodels.AlertRule,
|
||||
provenanceMap map[string]ngmodels.Provenance,
|
||||
namespaceMapper request.NamespaceMapper,
|
||||
continueToken string,
|
||||
) (*model.RecordingRuleList, error) {
|
||||
k8sRules := &model.RecordingRuleList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
Continue: continueToken,
|
||||
},
|
||||
Items: make([]model.RecordingRule, 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.RecordingRule) (*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.RecordingRule) (*ngmodels.AlertRule, error) {
|
||||
domainRule := &ngmodels.AlertRule{
|
||||
OrgID: orgID,
|
||||
UID: k8sRule.Name,
|
||||
Title: k8sRule.Spec.Title,
|
||||
Data: make([]ngmodels.AlertQuery, 0, len(k8sRule.Spec.Data)),
|
||||
IsPaused: k8sRule.Spec.Paused != nil && *k8sRule.Spec.Paused,
|
||||
Labels: make(map[string]string),
|
||||
|
||||
Record: &ngmodels.Record{
|
||||
Metric: k8sRule.Spec.Metric,
|
||||
TargetDatasourceUID: k8sRule.Spec.TargetDatasourceUID,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
meta, err := utils.MetaAccessor(k8sRule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||
}
|
||||
|
||||
domainRule.NamespaceUID = meta.GetFolder()
|
||||
|
||||
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 k, v := range k8sRule.Spec.Labels {
|
||||
domainRule.Labels[k] = string(v)
|
||||
}
|
||||
for refID, query := range k8sRule.Spec.Data {
|
||||
modelJson, err := json.Marshal(query.Model)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal model: %w", err)
|
||||
}
|
||||
domainQuery := ngmodels.AlertQuery{
|
||||
RefID: refID,
|
||||
QueryType: query.QueryType,
|
||||
DatasourceUID: string(query.DatasourceUID),
|
||||
Model: modelJson,
|
||||
}
|
||||
if query.RelativeTimeRange != nil {
|
||||
from, err := prom_model.ParseDuration(string(query.RelativeTimeRange.From))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration: %w", err)
|
||||
}
|
||||
to, err := prom_model.ParseDuration(string(query.RelativeTimeRange.To))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration: %w", err)
|
||||
}
|
||||
domainQuery.RelativeTimeRange = ngmodels.RelativeTimeRange{
|
||||
From: ngmodels.Duration(from),
|
||||
To: ngmodels.Duration(to),
|
||||
}
|
||||
}
|
||||
|
||||
domainRule.Data = append(domainRule.Data, domainQuery)
|
||||
|
||||
if query.Source != nil && *query.Source {
|
||||
if domainRule.Record.From != "" {
|
||||
return nil, fmt.Errorf("multiple queries marked as source: %s and %s", domainRule.Record.From, refID)
|
||||
}
|
||||
domainRule.Record.From = refID
|
||||
}
|
||||
}
|
||||
if domainRule.Record.From == "" {
|
||||
return nil, fmt.Errorf("no query marked as source")
|
||||
}
|
||||
return domainRule, nil
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package recordingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ grafanarest.Storage = (*legacyStorage)(nil)
|
||||
)
|
||||
|
||||
type legacyStorage struct {
|
||||
service provisioning.AlertRuleService
|
||||
namespacer request.NamespaceMapper
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
func (s *legacyStorage) New() runtime.Object {
|
||||
return ResourceInfo.NewFunc()
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Destroy() {}
|
||||
|
||||
func (s *legacyStorage) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *legacyStorage) GetSingularName() string {
|
||||
return ResourceInfo.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *legacyStorage) NewList() runtime.Object {
|
||||
return ResourceInfo.NewListFunc()
|
||||
}
|
||||
|
||||
func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, provenanceMap, continueToken, err := s.service.ListAlertRules(ctx, user, provisioning.ListAlertRulesOptions{
|
||||
RuleType: ngmodels.RuleTypeFilterRecording,
|
||||
Limit: opts.Limit,
|
||||
ContinueToken: opts.Continue,
|
||||
// TODO: add field selectors for filtering
|
||||
// TODO: add label selectors for filtering on group and folders
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertToK8sResources(info.OrgID, rules, provenanceMap, s.namespacer, continueToken)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rule, provenance, err := s.service.GetAlertRule(ctx, user, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
|
||||
return nil, k8serrors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, err := convertToK8sResource(info.OrgID, &rule, provenance, s.namespacer)
|
||||
if err != nil && errors.Is(err, errInvalidRule) {
|
||||
return nil, k8serrors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, ok := obj.(*model.RecordingRule)
|
||||
if !ok {
|
||||
return nil, k8serrors.NewBadRequest("expected valid recording rule object")
|
||||
}
|
||||
|
||||
if p.GenerateName != "" {
|
||||
return nil, k8serrors.NewBadRequest("generate-name is not supported in legacy storage mode")
|
||||
}
|
||||
// TODO: move this to the validation function
|
||||
if p.Labels[model.GroupLabelKey] != "" || p.Labels[model.GroupIndexLabelKey] != "" {
|
||||
return nil, k8serrors.NewBadRequest("cannot set group label when creating recording rule")
|
||||
}
|
||||
|
||||
model, provenance, err := convertToDomainModel(info.OrgID, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rule, err := s.service.CreateAlertRule(ctx, user, *model, provenance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertToK8sResource(info.OrgID, &rule, provenance, s.namespacer)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, _ rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
old, err := s.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
current, ok := old.(*model.RecordingRule)
|
||||
if !ok {
|
||||
return nil, false, k8serrors.NewBadRequest("expected valid recording rule object")
|
||||
}
|
||||
|
||||
obj, err := objInfo.UpdatedObject(ctx, old)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
if updateValidation != nil {
|
||||
if err := updateValidation(ctx, obj, old); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
new, ok := obj.(*model.RecordingRule)
|
||||
if !ok {
|
||||
return nil, false, k8serrors.NewBadRequest("expected valid recording rule object")
|
||||
}
|
||||
// FIXME(@rwwiv): this shouldn't be necessary
|
||||
if new.Name != "" {
|
||||
new.UID = types.UID(new.Name)
|
||||
}
|
||||
// TODO: move to validation function
|
||||
if current.Labels[model.GroupLabelKey] == "" && new.Labels[model.GroupLabelKey] != "" {
|
||||
return nil, false, k8serrors.NewBadRequest("cannot set group label when updating un-grouped recording rule")
|
||||
}
|
||||
|
||||
model, provenance, err := convertToDomainModel(info.OrgID, new)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// ignore returned rule as it doesn't contain the updated version
|
||||
_, err = s.service.UpdateAlertRule(ctx, user, *model, provenance)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
updated, provenance, err := s.service.GetAlertRule(ctx, user, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
rule, err := convertToK8sResource(info.OrgID, &updated, provenance, s.namespacer)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return rule, true, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, opts *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
old, err := s.Get(ctx, name, nil)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
if deleteValidation != nil {
|
||||
if err := deleteValidation(ctx, old); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
p, ok := old.(*model.RecordingRule)
|
||||
if !ok {
|
||||
return nil, false, k8serrors.NewBadRequest("expected valid recording rule object")
|
||||
}
|
||||
|
||||
sourceProv := p.GetProvenanceStatus()
|
||||
if !slices.Contains(model.AcceptedProvenanceStatuses, sourceProv) {
|
||||
return nil, false, fmt.Errorf("invalid provenance status: %s", sourceProv)
|
||||
}
|
||||
provenance := ngmodels.Provenance(sourceProv)
|
||||
|
||||
err = s.service.DeleteAlertRule(ctx, user, name, provenance)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
|
||||
return old, false, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) DeleteCollection(_ context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *internalversion.ListOptions) (runtime.Object, error) {
|
||||
// TODO: support this once a pattern is established for bulk delete operations
|
||||
return nil, k8serrors.NewMethodNotSupported(ResourceInfo.GroupResource(), "delete")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package recordingrule
|
||||
|
||||
import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
)
|
||||
|
||||
func NewStorage(legacySvc provisioning.AlertRuleService, namespacer request.NamespaceMapper) grafanarest.Storage {
|
||||
return &legacyStorage{
|
||||
service: legacySvc,
|
||||
namespacer: namespacer,
|
||||
tableConverter: rest.NewDefaultTableConvertor(ResourceInfo.GroupResource()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package recordingrule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
var kind = model.RecordingRuleKind()
|
||||
var ResourceInfo = utils.NewResourceInfo(kind.Group(), kind.Version(),
|
||||
kind.GroupVersionResource().Resource, strings.ToLower(kind.Kind()), kind.Kind(),
|
||||
func() runtime.Object { return kind.ZeroValue() },
|
||||
func() runtime.Object { return kind.ZeroListValue() },
|
||||
utils.TableColumns{},
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
||||
"github.com/grafana/grafana-app-sdk/simple"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/rules/pkg/apis"
|
||||
rulesApp "github.com/grafana/grafana/apps/alerting/rules/pkg/app"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules/alertrule"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules/recordingrule"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
_ appsdkapiserver.AppInstaller = (*AlertingRulesAppInstaller)(nil)
|
||||
_ appinstaller.AuthorizerProvider = (*AlertingRulesAppInstaller)(nil)
|
||||
_ appinstaller.LegacyStorageProvider = (*AlertingRulesAppInstaller)(nil)
|
||||
)
|
||||
|
||||
type AlertingRulesAppInstaller struct {
|
||||
appsdkapiserver.AppInstaller
|
||||
cfg *setting.Cfg
|
||||
ng *ngalert.AlertNG
|
||||
}
|
||||
|
||||
func RegisterAppInstaller(
|
||||
cfg *setting.Cfg,
|
||||
ng *ngalert.AlertNG,
|
||||
) (*AlertingRulesAppInstaller, error) {
|
||||
if ng.IsDisabled() {
|
||||
return nil, fmt.Errorf("alerting rules app installer cannot be registered when ngalert is disabled")
|
||||
}
|
||||
|
||||
installer := &AlertingRulesAppInstaller{
|
||||
cfg: cfg,
|
||||
ng: ng,
|
||||
}
|
||||
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), nil, rulesApp.New)
|
||||
|
||||
appConfig := app.Config{
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
}
|
||||
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.ManifestGoTypeAssociator, apis.ManifestCustomRouteResponsesAssociator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
installer.AppInstaller = i
|
||||
return installer, nil
|
||||
}
|
||||
|
||||
func (a *AlertingRulesAppInstaller) GetAuthorizer() authorizer.Authorizer {
|
||||
authz := a.ng.Api.AccessControl
|
||||
return authorizer.AuthorizerFunc(
|
||||
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
switch a.GetResource() {
|
||||
case recordingrule.ResourceInfo.GroupResource().Resource:
|
||||
return recordingrule.Authorize(ctx, authz, a)
|
||||
case alertrule.ResourceInfo.GroupResource().Resource:
|
||||
return alertrule.Authorize(ctx, authz, a)
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (a *AlertingRulesAppInstaller) GetLegacyStorage(gvr schema.GroupVersionResource) grafanarest.Storage {
|
||||
namespacer := request.GetNamespaceMapper(a.cfg)
|
||||
switch gvr {
|
||||
case recordingrule.ResourceInfo.GroupVersionResource():
|
||||
return recordingrule.NewStorage(*a.ng.Api.AlertRules, namespacer)
|
||||
case alertrule.ResourceInfo.GroupVersionResource():
|
||||
return alertrule.NewStorage(*a.ng.Api.AlertRules, namespacer)
|
||||
default:
|
||||
panic("unknown legacy storage requested: " + gvr.String())
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/advisor"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/investigations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/plugins"
|
||||
@@ -30,14 +31,15 @@ func ProvideAppInstallers(
|
||||
playlistAppInstaller *playlist.PlaylistAppInstaller,
|
||||
pluginsApplInstaller *plugins.PluginsAppInstaller,
|
||||
shorturlAppInstaller *shorturl.ShortURLAppInstaller,
|
||||
rulesAppInstaller *rules.AlertingRulesAppInstaller,
|
||||
) []appsdkapiserver.AppInstaller {
|
||||
installers := []appsdkapiserver.AppInstaller{
|
||||
playlistAppInstaller,
|
||||
pluginsApplInstaller,
|
||||
}
|
||||
installers := []appsdkapiserver.AppInstaller{playlistAppInstaller, pluginsApplInstaller}
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
|
||||
installers = append(installers, shorturlAppInstaller)
|
||||
}
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesAlertingRules) {
|
||||
installers = append(installers, rulesAppInstaller)
|
||||
}
|
||||
return installers
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apps/advisor"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/investigations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/plugins"
|
||||
@@ -20,4 +21,5 @@ var WireSet = wire.NewSet(
|
||||
notifications.RegisterApp,
|
||||
plugins.RegisterAppInstaller,
|
||||
shorturl.RegisterAppInstaller,
|
||||
rules.RegisterAppInstaller,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user