Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit b7f422b

Browse files
authored
Alerting: Receiver API Get+List+Delete (grafana#90384)
1 parent efdb08e commit b7f422b

File tree

14 files changed

+233
-67
lines changed

14 files changed

+233
-67
lines changed

pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package v0alpha1
22

3+
import "encoding/json"
4+
35
// Integration defines model for Integration.
46
// +k8s:openapi-gen=true
57
type Integration struct {
68
DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"`
79
// +mapType=atomic
810
SecureFields map[string]bool `json:"SecureFields,omitempty"`
911
// +listType=atomic
10-
Settings []byte `json:"settings"`
11-
Type string `json:"type"`
12-
Uid *string `json:"uid,omitempty"`
12+
Settings json.RawMessage `json:"settings"`
13+
Type string `json:"type"`
14+
Uid *string `json:"uid,omitempty"`
1315
}
1416

1517
// ReceiverSpec defines model for Spec.

pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go

+14-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/registry/apis/alerting/notifications/receiver/conversions.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package receiver
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"hash/fnv"
67

@@ -48,7 +49,7 @@ func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver,
4849
Uid: &integration.UID,
4950
Type: integration.Type,
5051
DisableResolveMessage: &integration.DisableResolveMessage,
51-
Settings: integration.Settings,
52+
Settings: json.RawMessage(integration.Settings),
5253
SecureFields: integration.SecureFields,
5354
})
5455
}
@@ -83,7 +84,7 @@ func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiRece
8384
grafanaIntegration := definitions.GettableGrafanaReceiver{
8485
Name: receiver.Spec.Title,
8586
Type: integration.Type,
86-
Settings: integration.Settings,
87+
Settings: definitions.RawMessage(integration.Settings),
8788
SecureFields: integration.SecureFields,
8889
//Provenance: "", //TODO: Convert provenance?
8990
}

pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go

+11-10
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,8 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
9393
return nil, err
9494
}
9595

96-
q := models.GetReceiverQuery{
96+
q := models.GetReceiversQuery{
9797
OrgID: info.OrgID,
98-
Name: uid, // TODO: Name/UID mapping or change signature of service.
9998
//Decrypt: ctx.QueryBool("decrypt"), // TODO: Query params.
10099
}
101100

@@ -104,12 +103,18 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
104103
return nil, err
105104
}
106105

107-
res, err := s.service.GetReceiver(ctx, q, user)
106+
res, err := s.service.GetReceivers(ctx, q, user)
108107
if err != nil {
109108
return nil, err
110109
}
111110

112-
return convertToK8sResource(info.OrgID, res, s.namespacer)
111+
for _, r := range res {
112+
if getUID(r) == uid {
113+
return convertToK8sResource(info.OrgID, r, s.namespacer)
114+
}
115+
}
116+
117+
return nil, errors.NewNotFound(resourceInfo.GroupResource(), uid)
113118
}
114119

115120
func (s *legacyStorage) Create(ctx context.Context,
@@ -211,13 +216,9 @@ func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation
211216
if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
212217
version = *options.Preconditions.ResourceVersion
213218
}
214-
p, ok := old.(*notifications.Receiver)
215-
if !ok {
216-
return nil, false, fmt.Errorf("expected receiver but got %s", old.GetObjectKind().GroupVersionKind())
217-
}
218219

219-
err = s.service.DeleteReceiver(ctx, p.Spec.Title, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option
220-
return old, false, err // false - will be deleted async
220+
err = s.service.DeleteReceiver(ctx, uid, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option
221+
return old, false, err // false - will be deleted async
221222
}
222223

223224
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {

pkg/registry/apis/alerting/notifications/register.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (t NotificationsAPIBuilder) GetAPIGroupInfo(
8080
return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err)
8181
}
8282

83-
recvStorage, err := receiver.NewStorage(nil, t.namespacer, scheme, optsGetter, dualWriteBuilder) // TODO: add receiver service
83+
recvStorage, err := receiver.NewStorage(t.ng.Api.ReceiverService, t.namespacer, scheme, optsGetter, dualWriteBuilder)
8484
if err != nil {
8585
return nil, fmt.Errorf("failed to initialize receiver storage: %w", err)
8686
}

pkg/services/ngalert/notifier/receiver_svc.go

+149-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import (
55
"encoding/base64"
66
"encoding/json"
77
"errors"
8+
"fmt"
9+
"hash/fnv"
810
"slices"
911

12+
"github.com/grafana/grafana/pkg/apimachinery/errutil"
1013
"github.com/grafana/grafana/pkg/apimachinery/identity"
1114
"github.com/grafana/grafana/pkg/infra/log"
1215
"github.com/grafana/grafana/pkg/services/accesscontrol"
1316
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
1417
"github.com/grafana/grafana/pkg/services/ngalert/models"
18+
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
1519
"github.com/grafana/grafana/pkg/services/secrets"
1620
)
1721

@@ -22,6 +26,11 @@ var (
2226
ErrNotFound = errors.New("not found") // TODO: convert to errutil
2327
)
2428

29+
var (
30+
ErrReceiverInUse = errutil.Conflict("alerting.notifications.receiver.used", errutil.WithPublicMessage("Receiver is used by one or many notification policies"))
31+
ErrVersionConflict = errutil.Conflict("alerting.notifications.receiver.conflict")
32+
)
33+
2534
// ReceiverService is the service for managing alertmanager receivers.
2635
type ReceiverService struct {
2736
ac accesscontrol.AccessControl
@@ -30,6 +39,7 @@ type ReceiverService struct {
3039
encryptionService secrets.Service
3140
xact transactionManager
3241
log log.Logger
42+
validator validation.ProvenanceStatusTransitionValidator
3343
}
3444

3545
type configStore interface {
@@ -39,6 +49,7 @@ type configStore interface {
3949

4050
type provisoningStore interface {
4151
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error)
52+
DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error
4253
}
4354

4455
type transactionManager interface {
@@ -60,6 +71,7 @@ func NewReceiverService(
6071
encryptionService: encryptionService,
6172
xact: xact,
6273
log: log,
74+
validator: validation.ValidateProvenanceRelaxed,
6375
}
6476
}
6577

@@ -119,7 +131,7 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
119131
return definitions.GettableApiReceiver{}, err
120132
}
121133

122-
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, "contactPoint")
134+
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
123135
if err != nil {
124136
return definitions.GettableApiReceiver{}, err
125137
}
@@ -158,7 +170,7 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
158170
return nil, err
159171
}
160172

161-
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, "contactPoint")
173+
provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
162174
if err != nil {
163175
return nil, err
164176
}
@@ -213,6 +225,83 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
213225
return output, nil
214226
}
215227

228+
// DeleteReceiver deletes a receiver by uid.
229+
// UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name.
230+
func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, orgID int64, callerProvenance definitions.Provenance, version string) error {
231+
//TODO: Check delete permissions.
232+
baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, orgID)
233+
if err != nil {
234+
return err
235+
}
236+
237+
cfg := definitions.PostableUserConfig{}
238+
err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg)
239+
if err != nil {
240+
return err
241+
}
242+
243+
idx, recv := getReceiverByUID(cfg, uid)
244+
if recv == nil {
245+
return ErrNotFound // TODO: nil?
246+
}
247+
248+
// TODO: Implement + check optimistic concurrency.
249+
250+
storedProvenance, err := rs.getContactPointProvenance(ctx, recv, orgID)
251+
if err != nil {
252+
return err
253+
}
254+
255+
if err := rs.validator(storedProvenance, models.Provenance(callerProvenance)); err != nil {
256+
return err
257+
}
258+
259+
if isReceiverInUse(recv.Name, []*definitions.Route{cfg.AlertmanagerConfig.Route}) {
260+
return ErrReceiverInUse.Errorf("")
261+
}
262+
263+
// Remove the receiver from the configuration.
264+
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:idx], cfg.AlertmanagerConfig.Receivers[idx+1:]...)
265+
266+
return rs.xact.InTransaction(ctx, func(ctx context.Context) error {
267+
serialized, err := json.Marshal(cfg)
268+
if err != nil {
269+
return err
270+
}
271+
cmd := models.SaveAlertmanagerConfigurationCmd{
272+
AlertmanagerConfiguration: string(serialized),
273+
ConfigurationVersion: baseCfg.ConfigurationVersion,
274+
FetchedConfigurationHash: baseCfg.ConfigurationHash,
275+
Default: false,
276+
OrgID: orgID,
277+
}
278+
279+
err = rs.cfgStore.UpdateAlertmanagerConfiguration(ctx, &cmd)
280+
if err != nil {
281+
return err
282+
}
283+
284+
// Remove provenance for all integrations in the receiver.
285+
for _, integration := range recv.GrafanaManagedReceivers {
286+
target := definitions.EmbeddedContactPoint{UID: integration.UID}
287+
if err := rs.provisioningStore.DeleteProvenance(ctx, &target, orgID); err != nil {
288+
return err
289+
}
290+
}
291+
return nil
292+
})
293+
}
294+
295+
func (rs *ReceiverService) CreateReceiver(ctx context.Context, r definitions.GettableApiReceiver, orgID int64) (definitions.GettableApiReceiver, error) {
296+
// TODO: Stub
297+
panic("not implemented")
298+
}
299+
300+
func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r definitions.GettableApiReceiver, orgID int64) (definitions.GettableApiReceiver, error) {
301+
// TODO: Stub
302+
panic("not implemented")
303+
}
304+
216305
func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, name, fallback string) func(value string) string {
217306
return func(value string) string {
218307
if !decrypt {
@@ -232,3 +321,61 @@ func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, na
232321
return string(decrypted)
233322
}
234323
}
324+
325+
// getContactPointProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations.
326+
func (rs *ReceiverService) getContactPointProvenance(ctx context.Context, r *definitions.PostableApiReceiver, orgID int64) (models.Provenance, error) {
327+
if len(r.GrafanaManagedReceivers) == 0 {
328+
return models.ProvenanceNone, nil
329+
}
330+
331+
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, orgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
332+
if err != nil {
333+
return "", err
334+
}
335+
336+
// Current provisioning works on the integration level, so we need some way to determine the provenance of the
337+
// entire receiver. All integrations in a receiver should have the same provenance, but we don't want to rely on
338+
// this assumption in case the first provenance is None and a later one is not. To this end, we return the first
339+
// non-zero provenance we find.
340+
for _, contactPoint := range r.GrafanaManagedReceivers {
341+
if p, exists := storedProvenances[contactPoint.UID]; exists && p != models.ProvenanceNone {
342+
return p, nil
343+
}
344+
}
345+
return models.ProvenanceNone, nil
346+
}
347+
348+
// getReceiverByUID returns the index and receiver with the given UID.
349+
func getReceiverByUID(cfg definitions.PostableUserConfig, uid string) (int, *definitions.PostableApiReceiver) {
350+
for i, r := range cfg.AlertmanagerConfig.Receivers {
351+
if getUID(r) == uid {
352+
return i, r
353+
}
354+
}
355+
return 0, nil
356+
}
357+
358+
// getUID returns the UID of a PostableApiReceiver.
359+
// Currently, the UID is a hash of the receiver name.
360+
func getUID(t *definitions.PostableApiReceiver) string { // TODO replace to stable UID when we switch to normal storage
361+
sum := fnv.New64()
362+
_, _ = sum.Write([]byte(t.Name))
363+
return fmt.Sprintf("%016x", sum.Sum64())
364+
}
365+
366+
// TODO: Check if the contact point is used directly in an alert rule.
367+
// isReceiverInUse checks if a receiver is used in a route or any of its sub-routes.
368+
func isReceiverInUse(name string, routes []*definitions.Route) bool {
369+
if len(routes) == 0 {
370+
return false
371+
}
372+
for _, route := range routes {
373+
if route.Receiver == name {
374+
return true
375+
}
376+
if isReceiverInUse(name, route.Routes) {
377+
return true
378+
}
379+
}
380+
return false
381+
}

0 commit comments

Comments
 (0)