mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 17:02:15 +08:00
Alerting: Receiver API Get+List+Delete (#90384)
This commit is contained in:
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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?
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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{
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
pkg/services/ngalert/provisioning/validation/errors.go
Normal file
29
pkg/services/ngalert/provisioning/validation/errors.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
@ -1,4 +1,4 @@
|
|||||||
package provisioning
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
Reference in New Issue
Block a user