From 4236845ea804f7fa62d7b0d9271b8199c75444c2 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 22 Oct 2025 17:11:32 -0400 Subject: [PATCH] register endpoint --- apps/alerting/notifications/pkg/app/app.go | 20 +++++ apps/alerting/notifications/pkg/app/config.go | 23 +++++ .../receiver/receivertesting/authorize.go | 26 ++++++ .../receiver/receivertesting/handler.go | 89 +++++++++++++++++++ .../apps/alerting/notifications/register.go | 9 +- 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 apps/alerting/notifications/pkg/app/config.go create mode 100644 pkg/registry/apps/alerting/notifications/receiver/receivertesting/authorize.go create mode 100644 pkg/registry/apps/alerting/notifications/receiver/receivertesting/handler.go diff --git a/apps/alerting/notifications/pkg/app/app.go b/apps/alerting/notifications/pkg/app/app.go index 65b3882ac59..dc3f1ebbeca 100644 --- a/apps/alerting/notifications/pkg/app/app.go +++ b/apps/alerting/notifications/pkg/app/app.go @@ -2,6 +2,8 @@ package app import ( "context" + "errors" + "fmt" "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" @@ -9,6 +11,7 @@ import ( "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" ) func New(cfg app.Config) (app.App, error) { @@ -19,6 +22,14 @@ func New(cfg app.Config) (app.App, error) { } } + customCfg, ok := cfg.SpecificConfig.(*Config) + if !ok { + return nil, errors.New("no configuration") + } + if err := customCfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + c := simple.AppConfig{ Name: "alerting.notification", KubeConfig: cfg.KubeConfig, @@ -30,6 +41,15 @@ func New(cfg app.Config) (app.App, error) { }, }, ManagedKinds: managedKinds, + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + v0alpha1.APIVersion: { + simple.AppVersionRoute{ + Namespaced: true, + Path: "testing/integration", + Method: "GET", + }: customCfg.ReceiverTestingHandler.HandleReceiverTestingRequest, + }, + }, } a, err := simple.NewApp(c) diff --git a/apps/alerting/notifications/pkg/app/config.go b/apps/alerting/notifications/pkg/app/config.go new file mode 100644 index 00000000000..16ae73b102e --- /dev/null +++ b/apps/alerting/notifications/pkg/app/config.go @@ -0,0 +1,23 @@ +package app + +import ( + "context" + "errors" + + "github.com/grafana/grafana-app-sdk/app" +) + +type Config struct { + ReceiverTestingHandler ReceiverTestingHandler +} + +func (c *Config) Validate() error { + if c.ReceiverTestingHandler == nil { + return errors.New("receiver testing handler is required") + } + return nil +} + +type ReceiverTestingHandler interface { + HandleReceiverTestingRequest(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error +} diff --git a/pkg/registry/apps/alerting/notifications/receiver/receivertesting/authorize.go b/pkg/registry/apps/alerting/notifications/receiver/receivertesting/authorize.go new file mode 100644 index 00000000000..a5cf2796f6a --- /dev/null +++ b/pkg/registry/apps/alerting/notifications/receiver/receivertesting/authorize.go @@ -0,0 +1,26 @@ +package receivertesting + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + eval := accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversTest), + ) + ok, err := ac.Evaluate(ctx, user, eval) + if ok { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", err +} diff --git a/pkg/registry/apps/alerting/notifications/receiver/receivertesting/handler.go b/pkg/registry/apps/alerting/notifications/receiver/receivertesting/handler.go new file mode 100644 index 00000000000..5e56cfb5cab --- /dev/null +++ b/pkg/registry/apps/alerting/notifications/receiver/receivertesting/handler.go @@ -0,0 +1,89 @@ +package receivertesting + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/grafana/grafana-app-sdk/app" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" + _ "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver" + "github.com/grafana/grafana/pkg/services/ngalert" + "github.com/grafana/grafana/pkg/services/ngalert/notifier" +) + +type ReceiverTestingHandler struct { + testingSvc *notifier.ReceiverTestingSvc +} + +func New(ng *ngalert.AlertNG) *ReceiverTestingHandler { + testingSvc := notifier.NewReceiverTestingSvc(ng.Api.ReceiverService, ng.MultiOrgAlertmanager, ng.SecretsService) + return &ReceiverTestingHandler{ + testingSvc: testingSvc, + } + +} + +func (p *ReceiverTestingHandler) HandleReceiverTestingRequest(ctx context.Context, w app.CustomRouteResponseWriter, r *app.CustomRouteRequest) error { + user, err := identity.GetRequester(ctx) + if err != nil { + return err + } + + var req v0alpha1.GetIntegrationTestRequestBody + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + writeBadRequest(w, err) + } + + alert := notifier.Alert{ + Labels: req.Alert.Labels, + Annotations: req.Alert.Annotations, + } + + integration, secure, err := receiver.ConvertReceiverIntegrationToIntegration("test-receiver", v0alpha1.ReceiverIntegration(req.Integration)) + if err != nil { + writeBadRequest(w, err) + } + receiverUID := "" + if req.ReceiverRef != nil { + receiverUID = *req.ReceiverRef + } + + result, err := p.testingSvc.Test(ctx, user, alert, receiverUID, integration, secure) + if err != nil { + // TODO better error handling + writeBadRequest(w, err) + } + + response := v0alpha1.GetIntegrationTest{ + TypeMeta: metav1.TypeMeta{}, + GetIntegrationTestBody: v0alpha1.GetIntegrationTestBody{ + Timestamp: time.Time(result.LastNotifyAttempt), + Duration: result.LastNotifyAttemptDuration, + }, + } + if result.LastNotifyAttemptError != "" { + response.GetIntegrationTestBody.Error = &result.LastNotifyAttemptError + } + + json, err := json.Marshal(response) + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) + } + + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(json) + return nil +} + +func writeBadRequest(w app.CustomRouteResponseWriter, err error) { + w.WriteHeader(400) + _, _ = w.Write([]byte(err.Error())) +} diff --git a/pkg/registry/apps/alerting/notifications/register.go b/pkg/registry/apps/alerting/notifications/register.go index c96fa1759b7..e31e409e462 100644 --- a/pkg/registry/apps/alerting/notifications/register.go +++ b/pkg/registry/apps/alerting/notifications/register.go @@ -15,6 +15,7 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver" + "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver/receivertesting" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/templategroup" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval" @@ -53,6 +54,10 @@ func RegisterAppInstaller( ng: ng, } + customCfg := notificationsApp.Config{ + ReceiverTestingHandler: receivertesting.New(ng), + } + localManifest := apis.LocalManifest() provider := simple.NewAppProvider(localManifest, nil, notificationsApp.New) @@ -60,7 +65,7 @@ func RegisterAppInstaller( appConfig := app.Config{ KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method ManifestData: *localManifest.ManifestData, - SpecificConfig: nil, + SpecificConfig: &customCfg, } i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{}) @@ -85,6 +90,8 @@ func (a AlertingNotificationsAppInstaller) GetAuthorizer() authorizer.Authorizer return receiver.Authorize(ctx, ac.NewReceiverAccess[*ngmodels.Receiver](authz, false), a) case routingtree.ResourceInfo.GroupResource().Resource: return routingtree.Authorize(ctx, authz, a) + case "testing": + return receivertesting.Authorize(ctx, authz, a) } return authorizer.DecisionNoOpinion, "", nil })