Alerting: Receiver API Get+List+Delete (#90384)

This commit is contained in:
Matthew Jacobson
2024-07-16 10:02:16 -04:00
committed by GitHub
parent efdb08ed8c
commit b7f422b68d
14 changed files with 233 additions and 67 deletions

View File

@ -1,5 +1,7 @@
package v0alpha1 package v0alpha1
import "encoding/json"
// Integration defines model for Integration. // Integration defines model for Integration.
// +k8s:openapi-gen=true // +k8s:openapi-gen=true
type Integration struct { type Integration struct {
@ -7,9 +9,9 @@ type Integration struct {
// +mapType=atomic // +mapType=atomic
SecureFields map[string]bool `json:"SecureFields,omitempty"` SecureFields map[string]bool `json:"SecureFields,omitempty"`
// +listType=atomic // +listType=atomic
Settings []byte `json:"settings"` Settings json.RawMessage `json:"settings"`
Type string `json:"type"` Type string `json:"type"`
Uid *string `json:"uid,omitempty"` Uid *string `json:"uid,omitempty"`
} }
// ReceiverSpec defines model for Spec. // ReceiverSpec defines model for Spec.

View File

@ -8,6 +8,8 @@
package v0alpha1 package v0alpha1
import ( import (
json "encoding/json"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@ -28,7 +30,7 @@ func (in *Integration) DeepCopyInto(out *Integration) {
} }
if in.Settings != nil { if in.Settings != nil {
in, out := &in.Settings, &out.Settings in, out := &in.Settings, &out.Settings
*out = make([]byte, len(*in)) *out = make(json.RawMessage, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.Uid != nil { if in.Uid != nil {

View File

@ -4,14 +4,18 @@
package v0alpha1 package v0alpha1
import (
json "encoding/json"
)
// IntegrationApplyConfiguration represents an declarative configuration of the Integration type for use // IntegrationApplyConfiguration represents an declarative configuration of the Integration type for use
// with apply. // with apply.
type IntegrationApplyConfiguration struct { type IntegrationApplyConfiguration struct {
DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"`
SecureFields map[string]bool `json:"SecureFields,omitempty"` SecureFields map[string]bool `json:"SecureFields,omitempty"`
Settings []byte `json:"settings,omitempty"` Settings *json.RawMessage `json:"settings,omitempty"`
Type *string `json:"type,omitempty"` Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"` Uid *string `json:"uid,omitempty"`
} }
// IntegrationApplyConfiguration constructs an declarative configuration of the Integration type for use with // IntegrationApplyConfiguration constructs an declarative configuration of the Integration type for use with
@ -42,13 +46,11 @@ func (b *IntegrationApplyConfiguration) WithSecureFields(entries map[string]bool
return b return b
} }
// WithSettings adds the given value to the Settings field in the declarative configuration // WithSettings sets the Settings field in the declarative configuration to the given value
// and returns the receiver, so that objects can be build by chaining "With" function invocations. // and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Settings field. // If called multiple times, the Settings field is set to the value of the last call.
func (b *IntegrationApplyConfiguration) WithSettings(values ...byte) *IntegrationApplyConfiguration { func (b *IntegrationApplyConfiguration) WithSettings(value json.RawMessage) *IntegrationApplyConfiguration {
for i := range values { b.Settings = &value
b.Settings = append(b.Settings, values[i])
}
return b return b
} }

View File

@ -1,6 +1,7 @@
package receiver package receiver
import ( import (
"encoding/json"
"fmt" "fmt"
"hash/fnv" "hash/fnv"
@ -48,7 +49,7 @@ func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver,
Uid: &integration.UID, Uid: &integration.UID,
Type: integration.Type, Type: integration.Type,
DisableResolveMessage: &integration.DisableResolveMessage, DisableResolveMessage: &integration.DisableResolveMessage,
Settings: integration.Settings, Settings: json.RawMessage(integration.Settings),
SecureFields: integration.SecureFields, SecureFields: integration.SecureFields,
}) })
} }
@ -83,7 +84,7 @@ func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiRece
grafanaIntegration := definitions.GettableGrafanaReceiver{ grafanaIntegration := definitions.GettableGrafanaReceiver{
Name: receiver.Spec.Title, Name: receiver.Spec.Title,
Type: integration.Type, Type: integration.Type,
Settings: integration.Settings, Settings: definitions.RawMessage(integration.Settings),
SecureFields: integration.SecureFields, SecureFields: integration.SecureFields,
//Provenance: "", //TODO: Convert provenance? //Provenance: "", //TODO: Convert provenance?
} }

View File

@ -93,9 +93,8 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
return nil, err return nil, err
} }
q := models.GetReceiverQuery{ q := models.GetReceiversQuery{
OrgID: info.OrgID, OrgID: info.OrgID,
Name: uid, // TODO: Name/UID mapping or change signature of service.
//Decrypt: ctx.QueryBool("decrypt"), // TODO: Query params. //Decrypt: ctx.QueryBool("decrypt"), // TODO: Query params.
} }
@ -104,12 +103,18 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
return nil, err return nil, err
} }
res, err := s.service.GetReceiver(ctx, q, user) res, err := s.service.GetReceivers(ctx, q, user)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return convertToK8sResource(info.OrgID, res, s.namespacer) for _, r := range res {
if getUID(r) == uid {
return convertToK8sResource(info.OrgID, r, s.namespacer)
}
}
return nil, errors.NewNotFound(resourceInfo.GroupResource(), uid)
} }
func (s *legacyStorage) Create(ctx context.Context, func (s *legacyStorage) Create(ctx context.Context,
@ -211,13 +216,9 @@ func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation
if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil { if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
version = *options.Preconditions.ResourceVersion version = *options.Preconditions.ResourceVersion
} }
p, ok := old.(*notifications.Receiver)
if !ok {
return nil, false, fmt.Errorf("expected receiver but got %s", old.GetObjectKind().GroupVersionKind())
}
err = s.service.DeleteReceiver(ctx, p.Spec.Title, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option err = s.service.DeleteReceiver(ctx, uid, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option
return old, false, err // false - will be deleted async return old, false, err // false - will be deleted async
} }
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {

View File

@ -80,7 +80,7 @@ func (t NotificationsAPIBuilder) GetAPIGroupInfo(
return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err) return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err)
} }
recvStorage, err := receiver.NewStorage(nil, t.namespacer, scheme, optsGetter, dualWriteBuilder) // TODO: add receiver service recvStorage, err := receiver.NewStorage(t.ng.Api.ReceiverService, t.namespacer, scheme, optsGetter, dualWriteBuilder)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize receiver storage: %w", err) return nil, fmt.Errorf("failed to initialize receiver storage: %w", err)
} }

View File

@ -5,13 +5,17 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"hash/fnv"
"slices" "slices"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
) )
@ -22,6 +26,11 @@ var (
ErrNotFound = errors.New("not found") // TODO: convert to errutil ErrNotFound = errors.New("not found") // TODO: convert to errutil
) )
var (
ErrReceiverInUse = errutil.Conflict("alerting.notifications.receiver.used", errutil.WithPublicMessage("Receiver is used by one or many notification policies"))
ErrVersionConflict = errutil.Conflict("alerting.notifications.receiver.conflict")
)
// ReceiverService is the service for managing alertmanager receivers. // ReceiverService is the service for managing alertmanager receivers.
type ReceiverService struct { type ReceiverService struct {
ac accesscontrol.AccessControl ac accesscontrol.AccessControl
@ -30,6 +39,7 @@ type ReceiverService struct {
encryptionService secrets.Service encryptionService secrets.Service
xact transactionManager xact transactionManager
log log.Logger log log.Logger
validator validation.ProvenanceStatusTransitionValidator
} }
type configStore interface { type configStore interface {
@ -39,6 +49,7 @@ type configStore interface {
type provisoningStore interface { type provisoningStore interface {
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error)
DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error
} }
type transactionManager interface { type transactionManager interface {
@ -60,6 +71,7 @@ func NewReceiverService(
encryptionService: encryptionService, encryptionService: encryptionService,
xact: xact, xact: xact,
log: log, log: log,
validator: validation.ValidateProvenanceRelaxed,
} }
} }
@ -119,7 +131,7 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
return definitions.GettableApiReceiver{}, err return definitions.GettableApiReceiver{}, err
} }
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, "contactPoint") provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
if err != nil { if err != nil {
return definitions.GettableApiReceiver{}, err return definitions.GettableApiReceiver{}, err
} }
@ -158,7 +170,7 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
return nil, err return nil, err
} }
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, "contactPoint") provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -213,6 +225,83 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
return output, nil return output, nil
} }
// DeleteReceiver deletes a receiver by uid.
// UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name.
func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, orgID int64, callerProvenance definitions.Provenance, version string) error {
//TODO: Check delete permissions.
baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, orgID)
if err != nil {
return err
}
cfg := definitions.PostableUserConfig{}
err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg)
if err != nil {
return err
}
idx, recv := getReceiverByUID(cfg, uid)
if recv == nil {
return ErrNotFound // TODO: nil?
}
// TODO: Implement + check optimistic concurrency.
storedProvenance, err := rs.getContactPointProvenance(ctx, recv, orgID)
if err != nil {
return err
}
if err := rs.validator(storedProvenance, models.Provenance(callerProvenance)); err != nil {
return err
}
if isReceiverInUse(recv.Name, []*definitions.Route{cfg.AlertmanagerConfig.Route}) {
return ErrReceiverInUse.Errorf("")
}
// Remove the receiver from the configuration.
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:idx], cfg.AlertmanagerConfig.Receivers[idx+1:]...)
return rs.xact.InTransaction(ctx, func(ctx context.Context) error {
serialized, err := json.Marshal(cfg)
if err != nil {
return err
}
cmd := models.SaveAlertmanagerConfigurationCmd{
AlertmanagerConfiguration: string(serialized),
ConfigurationVersion: baseCfg.ConfigurationVersion,
FetchedConfigurationHash: baseCfg.ConfigurationHash,
Default: false,
OrgID: orgID,
}
err = rs.cfgStore.UpdateAlertmanagerConfiguration(ctx, &cmd)
if err != nil {
return err
}
// Remove provenance for all integrations in the receiver.
for _, integration := range recv.GrafanaManagedReceivers {
target := definitions.EmbeddedContactPoint{UID: integration.UID}
if err := rs.provisioningStore.DeleteProvenance(ctx, &target, orgID); err != nil {
return err
}
}
return nil
})
}
func (rs *ReceiverService) CreateReceiver(ctx context.Context, r definitions.GettableApiReceiver, orgID int64) (definitions.GettableApiReceiver, error) {
// TODO: Stub
panic("not implemented")
}
func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r definitions.GettableApiReceiver, orgID int64) (definitions.GettableApiReceiver, error) {
// TODO: Stub
panic("not implemented")
}
func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, name, fallback string) func(value string) string { func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, name, fallback string) func(value string) string {
return func(value string) string { return func(value string) string {
if !decrypt { if !decrypt {
@ -232,3 +321,61 @@ func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, na
return string(decrypted) return string(decrypted)
} }
} }
// getContactPointProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations.
func (rs *ReceiverService) getContactPointProvenance(ctx context.Context, r *definitions.PostableApiReceiver, orgID int64) (models.Provenance, error) {
if len(r.GrafanaManagedReceivers) == 0 {
return models.ProvenanceNone, nil
}
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, orgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
if err != nil {
return "", err
}
// Current provisioning works on the integration level, so we need some way to determine the provenance of the
// entire receiver. All integrations in a receiver should have the same provenance, but we don't want to rely on
// this assumption in case the first provenance is None and a later one is not. To this end, we return the first
// non-zero provenance we find.
for _, contactPoint := range r.GrafanaManagedReceivers {
if p, exists := storedProvenances[contactPoint.UID]; exists && p != models.ProvenanceNone {
return p, nil
}
}
return models.ProvenanceNone, nil
}
// getReceiverByUID returns the index and receiver with the given UID.
func getReceiverByUID(cfg definitions.PostableUserConfig, uid string) (int, *definitions.PostableApiReceiver) {
for i, r := range cfg.AlertmanagerConfig.Receivers {
if getUID(r) == uid {
return i, r
}
}
return 0, nil
}
// getUID returns the UID of a PostableApiReceiver.
// Currently, the UID is a hash of the receiver name.
func getUID(t *definitions.PostableApiReceiver) string { // TODO replace to stable UID when we switch to normal storage
sum := fnv.New64()
_, _ = sum.Write([]byte(t.Name))
return fmt.Sprintf("%016x", sum.Sum64())
}
// TODO: Check if the contact point is used directly in an alert rule.
// isReceiverInUse checks if a receiver is used in a route or any of its sub-routes.
func isReceiverInUse(name string, routes []*definitions.Route) bool {
if len(routes) == 0 {
return false
}
for _, route := range routes {
if route.Receiver == name {
return true
}
if isReceiverInUse(name, route.Routes) {
return true
}
}
return false
}

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/secrets/database"
@ -183,12 +184,13 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *Receive
provisioningStore := fakes.NewFakeProvisioningStore() provisioningStore := fakes.NewFakeProvisioningStore()
return &ReceiverService{ return &ReceiverService{
acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), ac: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
provisioningStore, provisioningStore: provisioningStore,
store, cfgStore: store,
encryptSvc, encryptionService: encryptSvc,
xact, xact: xact,
log.NewNopLogger(), log: log.NewNopLogger(),
validator: validation.ValidateProvenanceRelaxed,
} }
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
@ -485,7 +486,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
if err != nil { if err != nil {
return err return err
} }
if canUpdate := canUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate { if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance)
} }
} }
@ -502,7 +503,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
if err != nil { if err != nil {
return err return err
} }
if canUpdate := canUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate { if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance) return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
} }
updates = append(updates, models.UpdateRule{ updates = append(updates, models.UpdateRule{

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/services/ngalert/models"
) )
var ErrValidation = fmt.Errorf("invalid object specification") var ErrValidation = fmt.Errorf("invalid object specification")
@ -16,11 +15,6 @@ var (
ErrNoAlertmanagerConfiguration = errutil.Internal("alerting.notification.configMissing", errutil.WithPublicMessage("No alertmanager configuration present in this organization")) ErrNoAlertmanagerConfiguration = errutil.Internal("alerting.notification.configMissing", errutil.WithPublicMessage("No alertmanager configuration present in this organization"))
ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one.")) ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one."))
ErrProvenanceChangeNotAllowed = errutil.Forbidden("alerting.notifications.invalidProvenance").MustTemplate(
"Resource with provenance status '{{ .Public.SourceProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'",
errutil.WithPublic("Resource with provenance status '{{ .Public.SourceProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'. You must use appropriate API to manage this resource"),
)
ErrVersionConflict = errutil.Conflict("alerting.notifications.conflict") ErrVersionConflict = errutil.Conflict("alerting.notifications.conflict")
ErrTimeIntervalNotFound = errutil.NotFound("alerting.notifications.time-intervals.notFound") ErrTimeIntervalNotFound = errutil.NotFound("alerting.notifications.time-intervals.notFound")
@ -53,19 +47,3 @@ func MakeErrTimeIntervalInvalid(err error) error {
return ErrTimeIntervalInvalid.Build(data) return ErrTimeIntervalInvalid.Build(data)
} }
func MakeErrProvenanceChangeNotAllowed(from, to models.Provenance) error {
if to == "" {
to = "none"
}
if from == "" {
from = "none"
}
data := errutil.TemplateData{
Public: map[string]interface{}{
"TargetProvenance": to,
"SourceProvenance": from,
},
}
return ErrProvenanceChangeNotAllowed.Build(data)
}

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
) )
type MuteTimingService struct { type MuteTimingService struct {
@ -20,7 +21,7 @@ type MuteTimingService struct {
provenanceStore ProvisioningStore provenanceStore ProvisioningStore
xact TransactionManager xact TransactionManager
log log.Logger log log.Logger
validator ProvenanceStatusTransitionValidator validator validation.ProvenanceStatusTransitionValidator
} }
func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *MuteTimingService { func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *MuteTimingService {
@ -29,7 +30,7 @@ func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact Tra
provenanceStore: prov, provenanceStore: prov,
xact: xact, xact: xact,
log: log, log: log,
validator: ValidateProvenanceRelaxed, validator: validation.ValidateProvenanceRelaxed,
} }
} }

View File

@ -0,0 +1,29 @@
package validation
import (
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
var (
ErrProvenanceChangeNotAllowed = errutil.Forbidden("alerting.notifications.invalidProvenance").MustTemplate(
"Resource with provenance status '{{ .Public.SourceProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'",
errutil.WithPublic("Resource with provenance status '{{ .Public.SourceProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'. You must use appropriate API to manage this resource"),
)
)
func MakeErrProvenanceChangeNotAllowed(from, to models.Provenance) error {
if to == "" {
to = "none"
}
if from == "" {
from = "none"
}
data := errutil.TemplateData{
Public: map[string]interface{}{
"TargetProvenance": to,
"SourceProvenance": from,
},
}
return ErrProvenanceChangeNotAllowed.Build(data)
}

View File

@ -1,12 +1,12 @@
package provisioning package validation
import ( import (
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
) )
// canUpdateProvenanceInRuleGroup checks if a provenance can be updated for a rule group and its alerts. // CanUpdateProvenanceInRuleGroup checks if a provenance can be updated for a rule group and its alerts.
// ReplaceRuleGroup function intends to replace an entire rule group: inserting, updating, and removing rules. // ReplaceRuleGroup function intends to replace an entire rule group: inserting, updating, and removing rules.
func canUpdateProvenanceInRuleGroup(storedProvenance, provenance models.Provenance) bool { func CanUpdateProvenanceInRuleGroup(storedProvenance, provenance models.Provenance) bool {
return storedProvenance == provenance || return storedProvenance == provenance ||
storedProvenance == models.ProvenanceNone || storedProvenance == models.ProvenanceNone ||
(storedProvenance == models.ProvenanceAPI && provenance == models.ProvenanceNone) (storedProvenance == models.ProvenanceAPI && provenance == models.ProvenanceNone)

View File

@ -1,4 +1,4 @@
package provisioning package validation
import ( import (
"fmt" "fmt"