Alerting: Add admission hooks for rules app (#113429)
This adds validating admission hooks to enforce the requirements on AlertRules and RecordingRules that are currently enforced through the provisioning service and storage mechanisms in preparation of a consistent validation in both legacy storage and unified storage. It also adds a mutating admission hook to the app to ensure that folder annotations and folder labels are kept in sync so we can perform label-selector lists.
This commit is contained in:
@@ -128,6 +128,10 @@ func convertToK8sResource(
|
||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||
}
|
||||
meta.SetFolder(rule.NamespaceUID)
|
||||
// Keep metadata label in sync with folder annotation for downstream consumers
|
||||
if rule.NamespaceUID != "" {
|
||||
k8sRule.Labels[model.FolderLabelKey] = rule.NamespaceUID
|
||||
}
|
||||
if rule.UpdatedBy != nil {
|
||||
meta.SetUpdatedBy(string(*rule.UpdatedBy))
|
||||
k8sRule.SetUpdatedBy(string(*rule.UpdatedBy))
|
||||
|
||||
@@ -76,6 +76,10 @@ func convertToK8sResource(
|
||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||
}
|
||||
meta.SetFolder(rule.NamespaceUID)
|
||||
// Keep metadata label in sync with folder annotation for downstream consumers
|
||||
if rule.NamespaceUID != "" {
|
||||
k8sRule.Labels[model.FolderLabelKey] = rule.NamespaceUID
|
||||
}
|
||||
if rule.UpdatedBy != nil {
|
||||
meta.SetUpdatedBy(string(*rule.UpdatedBy))
|
||||
k8sRule.SetUpdatedBy(string(*rule.UpdatedBy))
|
||||
|
||||
@@ -104,7 +104,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptio
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) {
|
||||
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
|
||||
@@ -114,6 +114,11 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, _ rest.V
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if createValidation != nil {
|
||||
if err := createValidation(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
p, ok := obj.(*model.RecordingRule)
|
||||
if !ok {
|
||||
|
||||
@@ -14,13 +14,17 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/rules/pkg/apis"
|
||||
rulesApp "github.com/grafana/grafana/apps/alerting/rules/pkg/app"
|
||||
rulesAppConfig "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"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"
|
||||
reqns "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -50,11 +54,66 @@ func RegisterAppInstaller(
|
||||
ng: ng,
|
||||
}
|
||||
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), nil, rulesApp.New)
|
||||
appSpecificConfig := rulesAppConfig.RuntimeConfig{
|
||||
// Validate folder existence using the folder service
|
||||
FolderValidator: func(ctx context.Context, folderUID string) (bool, error) {
|
||||
if folderUID == "" {
|
||||
return false, nil
|
||||
}
|
||||
orgID, err := reqns.OrgIDForList(ctx)
|
||||
user, _ := identity.GetRequester(ctx)
|
||||
if (err != nil || orgID < 1) && user != nil {
|
||||
orgID = user.GetOrgID()
|
||||
}
|
||||
if user == nil || orgID < 1 {
|
||||
// If we can't resolve identity/org in this context, don't block creation based on existence
|
||||
return true, nil
|
||||
}
|
||||
// Use the RuleStore to check namespace (folder) visibility
|
||||
_, err = ng.Api.RuleStore.GetNamespaceByUID(ctx, folderUID, orgID, user)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
BaseEvaluationInterval: ng.Cfg.UnifiedAlerting.BaseInterval,
|
||||
ReservedLabelKeys: ngmodels.LabelsUserCannotSpecify,
|
||||
// Validate that the configured notification receiver exists in the Alertmanager config
|
||||
NotificationSettingsValidator: func(ctx context.Context, receiver string) (bool, error) {
|
||||
if receiver == "" {
|
||||
return false, nil
|
||||
}
|
||||
orgID, err := reqns.OrgIDForList(ctx)
|
||||
if err != nil || orgID < 1 {
|
||||
if user, _ := identity.GetRequester(ctx); user != nil {
|
||||
orgID = user.GetOrgID()
|
||||
}
|
||||
}
|
||||
if orgID < 1 {
|
||||
// Without org context, skip validation rather than block
|
||||
return true, nil
|
||||
}
|
||||
provider := notifier.NewCachedNotificationSettingsValidationService(ng.Api.AlertingStore)
|
||||
vd, err := provider.Validator(ctx, orgID)
|
||||
if err != nil {
|
||||
log.New("alerting.rules.app").Error("failed to create notification settings validator", "error", err)
|
||||
// If we cannot build a validator, don't block admission
|
||||
return true, nil
|
||||
}
|
||||
// Only validate receiver presence; construct minimal settings
|
||||
if err := vd.Validate(ngmodels.NotificationSettings{Receiver: receiver}); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), appSpecificConfig, rulesApp.New)
|
||||
|
||||
appConfig := app.Config{
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
SpecificConfig: appSpecificConfig,
|
||||
}
|
||||
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{})
|
||||
@@ -81,7 +140,7 @@ func (a *AlertingRulesAppInstaller) GetAuthorizer() authorizer.Authorizer {
|
||||
}
|
||||
|
||||
func (a *AlertingRulesAppInstaller) GetLegacyStorage(gvr schema.GroupVersionResource) grafanarest.Storage {
|
||||
namespacer := request.GetNamespaceMapper(a.cfg)
|
||||
namespacer := reqns.GetNamespaceMapper(a.cfg)
|
||||
switch gvr {
|
||||
case recordingrule.ResourceInfo.GroupVersionResource():
|
||||
return recordingrule.NewStorage(*a.ng.Api.AlertRules, namespacer)
|
||||
|
||||
Reference in New Issue
Block a user