diff --git a/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go b/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go index 9adcef30e3f..0d1d942fd8d 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go +++ b/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go @@ -1,5 +1,7 @@ package v0alpha1 +import "encoding/json" + // Integration defines model for Integration. // +k8s:openapi-gen=true type Integration struct { @@ -7,9 +9,9 @@ type Integration struct { // +mapType=atomic SecureFields map[string]bool `json:"SecureFields,omitempty"` // +listType=atomic - Settings []byte `json:"settings"` - Type string `json:"type"` - Uid *string `json:"uid,omitempty"` + Settings json.RawMessage `json:"settings"` + Type string `json:"type"` + Uid *string `json:"uid,omitempty"` } // ReceiverSpec defines model for Spec. diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go index 69f57b975f8..47eb7b98351 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go @@ -8,6 +8,8 @@ package v0alpha1 import ( + json "encoding/json" + runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -28,7 +30,7 @@ func (in *Integration) DeepCopyInto(out *Integration) { } if in.Settings != nil { in, out := &in.Settings, &out.Settings - *out = make([]byte, len(*in)) + *out = make(json.RawMessage, len(*in)) copy(*out, *in) } if in.Uid != nil { diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go index b1bfb5e1be5..15f78fd342a 100644 --- a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go @@ -4,14 +4,18 @@ package v0alpha1 +import ( + json "encoding/json" +) + // IntegrationApplyConfiguration represents an declarative configuration of the Integration type for use // with apply. type IntegrationApplyConfiguration struct { - DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` - SecureFields map[string]bool `json:"SecureFields,omitempty"` - Settings []byte `json:"settings,omitempty"` - Type *string `json:"type,omitempty"` - Uid *string `json:"uid,omitempty"` + DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` + SecureFields map[string]bool `json:"SecureFields,omitempty"` + Settings *json.RawMessage `json:"settings,omitempty"` + Type *string `json:"type,omitempty"` + Uid *string `json:"uid,omitempty"` } // 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 } -// WithSettings adds the given value to the Settings field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, values provided by each call will be appended to the Settings field. -func (b *IntegrationApplyConfiguration) WithSettings(values ...byte) *IntegrationApplyConfiguration { - for i := range values { - b.Settings = append(b.Settings, values[i]) - } +// WithSettings sets the Settings field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Settings field is set to the value of the last call. +func (b *IntegrationApplyConfiguration) WithSettings(value json.RawMessage) *IntegrationApplyConfiguration { + b.Settings = &value return b } diff --git a/pkg/registry/apis/alerting/notifications/receiver/conversions.go b/pkg/registry/apis/alerting/notifications/receiver/conversions.go index b4a19200381..62da13ada3d 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/conversions.go +++ b/pkg/registry/apis/alerting/notifications/receiver/conversions.go @@ -1,6 +1,7 @@ package receiver import ( + "encoding/json" "fmt" "hash/fnv" @@ -48,7 +49,7 @@ func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver, Uid: &integration.UID, Type: integration.Type, DisableResolveMessage: &integration.DisableResolveMessage, - Settings: integration.Settings, + Settings: json.RawMessage(integration.Settings), SecureFields: integration.SecureFields, }) } @@ -83,7 +84,7 @@ func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiRece grafanaIntegration := definitions.GettableGrafanaReceiver{ Name: receiver.Spec.Title, Type: integration.Type, - Settings: integration.Settings, + Settings: definitions.RawMessage(integration.Settings), SecureFields: integration.SecureFields, //Provenance: "", //TODO: Convert provenance? } diff --git a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go index 8d7d7b641f4..f103d8e5017 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go @@ -93,9 +93,8 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption return nil, err } - q := models.GetReceiverQuery{ + q := models.GetReceiversQuery{ OrgID: info.OrgID, - Name: uid, // TODO: Name/UID mapping or change signature of service. //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 } - res, err := s.service.GetReceiver(ctx, q, user) + res, err := s.service.GetReceivers(ctx, q, user) if err != nil { 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, @@ -211,13 +216,9 @@ func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil { 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 - return old, false, err // false - will be deleted async + 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 } func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index 89492dd97c7..b214cd4cca1 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -80,7 +80,7 @@ func (t NotificationsAPIBuilder) GetAPIGroupInfo( 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 { return nil, fmt.Errorf("failed to initialize receiver storage: %w", err) } diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index 858d556da23..e7349eac53b 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -5,13 +5,17 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" + "hash/fnv" "slices" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "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/provisioning/validation" "github.com/grafana/grafana/pkg/services/secrets" ) @@ -22,6 +26,11 @@ var ( 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. type ReceiverService struct { ac accesscontrol.AccessControl @@ -30,6 +39,7 @@ type ReceiverService struct { encryptionService secrets.Service xact transactionManager log log.Logger + validator validation.ProvenanceStatusTransitionValidator } type configStore interface { @@ -39,6 +49,7 @@ type configStore interface { type provisoningStore interface { 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 { @@ -60,6 +71,7 @@ func NewReceiverService( encryptionService: encryptionService, xact: xact, log: log, + validator: validation.ValidateProvenanceRelaxed, } } @@ -119,7 +131,7 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver 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 { return definitions.GettableApiReceiver{}, err } @@ -158,7 +170,7 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive 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 { return nil, err } @@ -213,6 +225,83 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive 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 { return func(value string) string { if !decrypt { @@ -232,3 +321,61 @@ func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, na 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 +} diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 25dbcc50952..0d719d29746 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "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/provisioning/validation" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" @@ -183,12 +184,13 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *Receive provisioningStore := fakes.NewFakeProvisioningStore() return &ReceiverService{ - acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), - provisioningStore, - store, - encryptSvc, - xact, - log.NewNopLogger(), + ac: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), + provisioningStore: provisioningStore, + cfgStore: store, + encryptionService: encryptSvc, + xact: xact, + log: log.NewNopLogger(), + validator: validation.ValidateProvenanceRelaxed, } } diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 29953f863ae..c984b5a9730 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/models" "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/quota" "github.com/grafana/grafana/pkg/util" @@ -485,7 +486,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity if err != nil { 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) } } @@ -502,7 +503,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity if err != nil { 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) } updates = append(updates, models.UpdateRule{ diff --git a/pkg/services/ngalert/provisioning/errors.go b/pkg/services/ngalert/provisioning/errors.go index 4c164381816..3588e010712 100644 --- a/pkg/services/ngalert/provisioning/errors.go +++ b/pkg/services/ngalert/provisioning/errors.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/apimachinery/errutil" - "github.com/grafana/grafana/pkg/services/ngalert/models" ) 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")) 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") ErrTimeIntervalNotFound = errutil.NotFound("alerting.notifications.time-intervals.notFound") @@ -53,19 +47,3 @@ func MakeErrTimeIntervalInvalid(err error) error { 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) -} diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index e5a95464970..f4366e5541a 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "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/provisioning/validation" ) type MuteTimingService struct { @@ -20,7 +21,7 @@ type MuteTimingService struct { provenanceStore ProvisioningStore xact TransactionManager log log.Logger - validator ProvenanceStatusTransitionValidator + validator validation.ProvenanceStatusTransitionValidator } 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, xact: xact, log: log, - validator: ValidateProvenanceRelaxed, + validator: validation.ValidateProvenanceRelaxed, } } diff --git a/pkg/services/ngalert/provisioning/validation/errors.go b/pkg/services/ngalert/provisioning/validation/errors.go new file mode 100644 index 00000000000..74c0d51364b --- /dev/null +++ b/pkg/services/ngalert/provisioning/validation/errors.go @@ -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) +} diff --git a/pkg/services/ngalert/provisioning/provenance.go b/pkg/services/ngalert/provisioning/validation/provenance.go similarity index 87% rename from pkg/services/ngalert/provisioning/provenance.go rename to pkg/services/ngalert/provisioning/validation/provenance.go index 7f2adc651ec..4199cf5bfaf 100644 --- a/pkg/services/ngalert/provisioning/provenance.go +++ b/pkg/services/ngalert/provisioning/validation/provenance.go @@ -1,12 +1,12 @@ -package provisioning +package validation import ( "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. -func canUpdateProvenanceInRuleGroup(storedProvenance, provenance models.Provenance) bool { +func CanUpdateProvenanceInRuleGroup(storedProvenance, provenance models.Provenance) bool { return storedProvenance == provenance || storedProvenance == models.ProvenanceNone || (storedProvenance == models.ProvenanceAPI && provenance == models.ProvenanceNone) diff --git a/pkg/services/ngalert/provisioning/provenance_test.go b/pkg/services/ngalert/provisioning/validation/provenance_test.go similarity index 98% rename from pkg/services/ngalert/provisioning/provenance_test.go rename to pkg/services/ngalert/provisioning/validation/provenance_test.go index 6dc3640ba41..b98ce241b03 100644 --- a/pkg/services/ngalert/provisioning/provenance_test.go +++ b/pkg/services/ngalert/provisioning/validation/provenance_test.go @@ -1,4 +1,4 @@ -package provisioning +package validation import ( "fmt"