Compare commits

...

7 Commits

Author SHA1 Message Date
Yuri Tseretyan
4236845ea8 register endpoint 2026-01-05 12:47:21 -05:00
Yuri Tseretyan
d4e94cef50 define a custom route for receiver testing 2026-01-05 12:47:21 -05:00
Yuri Tseretyan
c723526f4e integration testing svc 2026-01-05 12:47:21 -05:00
Yuri Tseretyan
e56fc80d93 create testintegration method 2026-01-05 12:47:20 -05:00
Yuri Tseretyan
dfaa5ec1d4 refactor: extract conversion to integration to ConvertReceiverIntegrationToIntegration 2026-01-05 12:46:26 -05:00
Yuri Tseretyan
4abd88ec95 refactor: consolidate all encrypt\decrypt functions 2026-01-05 12:46:26 -05:00
Yuri Tseretyan
405871d41d refactor: change GetReceiver to get by UID
to avoid conversions of name to uid back an forth
2026-01-05 12:46:25 -05:00
25 changed files with 963 additions and 143 deletions

View File

@@ -430,4 +430,100 @@ spec:
type: object
scope: Namespaced
name: v0alpha1
routes:
namespaced:
/testing/integration:
get:
operationId: getIntegrationTest
requestBody:
content:
application/json:
schema:
additionalProperties: false
properties:
alert:
$ref: '#/components/schemas/getIntegrationTestAlert'
integration:
$ref: '#/components/schemas/getIntegrationTestIntegration'
receiver_ref:
type: string
required:
- alert
- integration
type: object
required: true
responses:
default:
content:
application/json:
schema:
additionalProperties: false
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of
this representation of an object. Servers should convert
recognized schemas to the latest internal value, and may
reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
duration:
type: string
error:
type: string
kind:
description: 'Kind is a string value representing the REST
resource this object represents. Servers may infer this
from the endpoint the client submits requests to. Cannot
be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
timestamp:
format: date-time
type: string
required:
- timestamp
- duration
- apiVersion
- kind
type: object
description: Default OK response
schemas:
getIntegrationTestAlert:
additionalProperties: false
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
required:
- labels
- annotations
type: object
getIntegrationTestIntegration:
additionalProperties: false
properties:
disableResolveMessage:
type: boolean
secureFields:
additionalProperties:
type: boolean
type: object
settings:
additionalProperties:
additionalProperties: {}
type: object
type: object
type:
type: string
uid:
type: string
version:
type: string
required:
- type
- version
- settings
type: object
served: true

View File

@@ -1,5 +1,19 @@
package kinds
import (
"time",
"github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1"
)
#Alert: {
labels: {
[string]: string
}
annotations: {
[string]: string
}
}
manifest: {
appName: "alerting-notifications"
groupOverride: "notifications.alerting.grafana.app"
@@ -14,7 +28,28 @@ manifest: {
routeTreev0alpha1,
templatev0alpha1,
timeIntervalv0alpha1,
]
],
routes: {
namespaced: {
"/testing/integration" : {
"GET": {
name: "getIntegrationTest"
request: {
body: {
alert: #Alert
receiver_ref?: string
integration: v0alpha1.#Integration
}
}
response: {
timestamp: time.Time
duration: string
error?: string
}
}
}
}
},
}
}
}

View File

@@ -0,0 +1,46 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
type GetIntegrationTestRequestAlert struct {
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
}
// NewGetIntegrationTestRequestAlert creates a new GetIntegrationTestRequestAlert object.
func NewGetIntegrationTestRequestAlert() *GetIntegrationTestRequestAlert {
return &GetIntegrationTestRequestAlert{
Labels: map[string]string{},
Annotations: map[string]string{},
}
}
type GetIntegrationTestRequestIntegration struct {
Uid *string `json:"uid,omitempty"`
Type string `json:"type"`
Version string `json:"version"`
DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"`
Settings map[string]any `json:"settings"`
SecureFields map[string]bool `json:"secureFields,omitempty"`
}
// NewGetIntegrationTestRequestIntegration creates a new GetIntegrationTestRequestIntegration object.
func NewGetIntegrationTestRequestIntegration() *GetIntegrationTestRequestIntegration {
return &GetIntegrationTestRequestIntegration{
Settings: map[string]any{},
}
}
type GetIntegrationTestRequestBody struct {
Alert GetIntegrationTestRequestAlert `json:"alert"`
ReceiverRef *string `json:"receiver_ref,omitempty"`
Integration GetIntegrationTestRequestIntegration `json:"integration"`
}
// NewGetIntegrationTestRequestBody creates a new GetIntegrationTestRequestBody object.
func NewGetIntegrationTestRequestBody() *GetIntegrationTestRequestBody {
return &GetIntegrationTestRequestBody{
Alert: *NewGetIntegrationTestRequestAlert(),
Integration: *NewGetIntegrationTestRequestIntegration(),
}
}

View File

@@ -0,0 +1,19 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
time "time"
)
// +k8s:openapi-gen=true
type GetIntegrationTestBody struct {
Timestamp time.Time `json:"timestamp"`
Duration string `json:"duration"`
Error *string `json:"error,omitempty"`
}
// NewGetIntegrationTestBody creates a new GetIntegrationTestBody object.
func NewGetIntegrationTestBody() *GetIntegrationTestBody {
return &GetIntegrationTestBody{}
}

View File

@@ -0,0 +1,37 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:openapi-gen=true
type GetIntegrationTest struct {
metav1.TypeMeta `json:",inline"`
GetIntegrationTestBody `json:",inline"`
}
func NewGetIntegrationTest() *GetIntegrationTest {
return &GetIntegrationTest{}
}
func (t *GetIntegrationTestBody) DeepCopyInto(dst *GetIntegrationTestBody) {
_ = resource.CopyObjectInto(dst, t)
}
func (o *GetIntegrationTest) DeepCopyObject() runtime.Object {
dst := NewGetIntegrationTest()
o.DeepCopyInto(dst)
return dst
}
func (o *GetIntegrationTest) DeepCopyInto(dst *GetIntegrationTest) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.GetIntegrationTestBody.DeepCopyInto(&dst.GetIntegrationTestBody)
}
var _ runtime.Object = NewGetIntegrationTest()

View File

@@ -79,9 +79,206 @@ var appManifestData = app.ManifestData{
},
},
Routes: app.ManifestVersionRoutes{
Namespaced: map[string]spec3.PathProps{},
Cluster: map[string]spec3.PathProps{},
Schemas: map[string]spec.Schema{},
Namespaced: map[string]spec3.PathProps{
"/testing/integration": {
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
OperationId: "getIntegrationTest",
RequestBody: &spec3.RequestBody{
RequestBodyProps: spec3.RequestBodyProps{
Required: true,
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"alert": {
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/getIntegrationTestAlert"),
},
},
"integration": {
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/getIntegrationTestIntegration"),
},
},
"receiver_ref": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
Required: []string{
"alert",
"integration",
},
}},
}},
},
}},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
Default: &spec3.Response{
ResponseProps: spec3.ResponseProps{
Description: "Default OK response",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"apiVersion": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
},
},
"duration": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"error": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"kind": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
},
},
"timestamp": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date-time",
},
},
},
Required: []string{
"timestamp",
"duration",
"apiVersion",
"kind",
},
}},
}},
},
},
},
}},
},
},
},
},
Cluster: map[string]spec3.PathProps{},
Schemas: map[string]spec.Schema{
"getIntegrationTestAlert": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"annotations": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
},
"labels": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
},
},
Required: []string{
"labels",
"annotations",
},
},
},
"getIntegrationTestIntegration": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"disableResolveMessage": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
},
},
"secureFields": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
},
},
},
},
},
"settings": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{},
},
},
},
},
},
},
},
"type": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"uid": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"version": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
Required: []string{
"type",
"version",
"settings",
},
},
},
},
},
},
},
@@ -109,7 +306,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist
return goType, exists
}
var customRouteToGoResponseType = map[string]any{}
var customRouteToGoResponseType = map[string]any{
"v0alpha1||<namespace>/testing/integration|GET": v0alpha1.GetIntegrationTest{},
}
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
@@ -133,7 +332,9 @@ func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goTyp
return goType, exists
}
var customRouteToGoRequestBodyType = map[string]any{}
var customRouteToGoRequestBodyType = map[string]any{
"v0alpha1||<namespace>/testing/integration|GET": v0alpha1.GetIntegrationTestRequestBody{},
}
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -128,47 +128,55 @@ func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[str
}
storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations))
for _, integration := range receiver.Spec.Integrations {
t, err := alertingNotify.IntegrationTypeFromString(integration.Type)
grafanaIntegration, secureFields, err := ConvertReceiverIntegrationToIntegration(receiver.Spec.Title, integration)
if err != nil {
return nil, nil, err
}
var config schema.IntegrationSchemaVersion
typeSchema, _ := alertingNotify.GetSchemaForIntegration(t)
if integration.Version != "" {
var ok bool
config, ok = typeSchema.GetVersion(schema.Version(integration.Version))
if !ok {
return nil, nil, fmt.Errorf("invalid version %s for integration type %s", integration.Version, integration.Type)
}
} else {
config = typeSchema.GetCurrentVersion()
}
grafanaIntegration := ngmodels.Integration{
Name: receiver.Spec.Title,
Config: config,
Settings: maps.Clone(integration.Settings),
SecureSettings: make(map[string]string),
}
if integration.Uid != nil {
grafanaIntegration.UID = *integration.Uid
}
if integration.DisableResolveMessage != nil {
grafanaIntegration.DisableResolveMessage = *integration.DisableResolveMessage
}
domain.Integrations = append(domain.Integrations, &grafanaIntegration)
if grafanaIntegration.UID != "" {
// This is an existing integration, so we track the secure fields being requested to copy over from existing values.
secureFields := make([]string, 0, len(integration.SecureFields))
for k, isSecure := range integration.SecureFields {
if isSecure {
secureFields = append(secureFields, k)
}
}
storedSecureFields[grafanaIntegration.UID] = secureFields
}
storedSecureFields[grafanaIntegration.UID] = secureFields
}
return domain, storedSecureFields, nil
}
func ConvertReceiverIntegrationToIntegration(receiverTitle string, integration model.ReceiverIntegration) (ngmodels.Integration, []string, error) {
t, err := alertingNotify.IntegrationTypeFromString(integration.Type)
if err != nil {
return ngmodels.Integration{}, nil, err
}
var config schema.IntegrationSchemaVersion
typeSchema, _ := alertingNotify.GetSchemaForIntegration(t)
if integration.Version != "" {
var ok bool
config, ok = typeSchema.GetVersion(schema.Version(integration.Version))
if !ok {
return ngmodels.Integration{}, nil, fmt.Errorf("invalid version %s for integration type %s", integration.Version, integration.Type)
}
} else {
config = typeSchema.GetCurrentVersion()
}
grafanaIntegration := ngmodels.Integration{
Name: receiverTitle,
Config: config,
Settings: maps.Clone(integration.Settings),
SecureSettings: make(map[string]string),
}
if integration.Uid != nil {
grafanaIntegration.UID = *integration.Uid
}
if integration.DisableResolveMessage != nil {
grafanaIntegration.DisableResolveMessage = *integration.DisableResolveMessage
}
var secureFields []string
if grafanaIntegration.UID != "" {
// This is an existing integration, so we track the secure fields being requested to copy over from existing values.
secureFields = make([]string, 0, len(integration.SecureFields))
for k, isSecure := range integration.SecureFields {
if isSecure {
secureFields = append(secureFields, k)
}
}
}
return grafanaIntegration, secureFields, nil
}

View File

@@ -25,7 +25,7 @@ var (
)
type ReceiverService interface {
GetReceiver(ctx context.Context, q ngmodels.GetReceiverQuery, user identity.Requester) (*ngmodels.Receiver, error)
GetReceiver(ctx context.Context, uid string, decrypt bool, user identity.Requester) (*ngmodels.Receiver, error)
GetReceivers(ctx context.Context, q ngmodels.GetReceiversQuery, user identity.Requester) ([]*ngmodels.Receiver, error)
CreateReceiver(ctx context.Context, r *ngmodels.Receiver, orgID int64, user identity.Requester) (*ngmodels.Receiver, error)
UpdateReceiver(ctx context.Context, r *ngmodels.Receiver, storedSecureFields map[string][]string, orgID int64, user identity.Requester) (*ngmodels.Receiver, error)
@@ -120,18 +120,13 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
if err != nil {
return nil, apierrors.NewNotFound(ResourceInfo.GroupResource(), uid)
}
q := ngmodels.GetReceiverQuery{
OrgID: info.OrgID,
Name: name,
Decrypt: false,
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
r, err := s.service.GetReceiver(ctx, q, user)
r, err := s.service.GetReceiver(ctx, name, false, user)
if err != nil {
return nil, err
}

View File

@@ -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
}

View File

@@ -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()))
}

View File

@@ -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
})

View File

@@ -813,6 +813,65 @@ func (_c *AlertmanagerMock_StopAndWait_Call) RunAndReturn(run func()) *Alertmana
return _c
}
// TestIntegration provides a mock function with given fields: ctx, receiverName, integrationConfig, alert
func (_m *AlertmanagerMock) TestIntegration(ctx context.Context, receiverName string, integrationConfig models.Integration, alert alertingmodels.TestReceiversConfigAlertParams) (alertingmodels.IntegrationStatus, error) {
ret := _m.Called(ctx, receiverName, integrationConfig, alert)
if len(ret) == 0 {
panic("no return value specified for TestIntegration")
}
var r0 alertingmodels.IntegrationStatus
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, models.Integration, alertingmodels.TestReceiversConfigAlertParams) (alertingmodels.IntegrationStatus, error)); ok {
return rf(ctx, receiverName, integrationConfig, alert)
}
if rf, ok := ret.Get(0).(func(context.Context, string, models.Integration, alertingmodels.TestReceiversConfigAlertParams) alertingmodels.IntegrationStatus); ok {
r0 = rf(ctx, receiverName, integrationConfig, alert)
} else {
r0 = ret.Get(0).(alertingmodels.IntegrationStatus)
}
if rf, ok := ret.Get(1).(func(context.Context, string, models.Integration, alertingmodels.TestReceiversConfigAlertParams) error); ok {
r1 = rf(ctx, receiverName, integrationConfig, alert)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AlertmanagerMock_TestIntegration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TestIntegration'
type AlertmanagerMock_TestIntegration_Call struct {
*mock.Call
}
// TestIntegration is a helper method to define mock.On call
// - ctx context.Context
// - receiverName string
// - integrationConfig models.Integration
// - alert alertingmodels.TestReceiversConfigAlertParams
func (_e *AlertmanagerMock_Expecter) TestIntegration(ctx interface{}, receiverName interface{}, integrationConfig interface{}, alert interface{}) *AlertmanagerMock_TestIntegration_Call {
return &AlertmanagerMock_TestIntegration_Call{Call: _e.mock.On("TestIntegration", ctx, receiverName, integrationConfig, alert)}
}
func (_c *AlertmanagerMock_TestIntegration_Call) Run(run func(ctx context.Context, receiverName string, integrationConfig models.Integration, alert alertingmodels.TestReceiversConfigAlertParams)) *AlertmanagerMock_TestIntegration_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(models.Integration), args[3].(alertingmodels.TestReceiversConfigAlertParams))
})
return _c
}
func (_c *AlertmanagerMock_TestIntegration_Call) Return(_a0 alertingmodels.IntegrationStatus, _a1 error) *AlertmanagerMock_TestIntegration_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AlertmanagerMock_TestIntegration_Call) RunAndReturn(run func(context.Context, string, models.Integration, alertingmodels.TestReceiversConfigAlertParams) (alertingmodels.IntegrationStatus, error)) *AlertmanagerMock_TestIntegration_Call {
_c.Call.Return(run)
return _c
}
// TestReceivers provides a mock function with given fields: ctx, c
func (_m *AlertmanagerMock) TestReceivers(ctx context.Context, c definitions.TestReceiversConfigBodyParams) (*notify.TestReceiversResult, int, error) {
ret := _m.Called(ctx, c)

View File

@@ -1,6 +1,9 @@
package notifier
import (
"encoding/json"
alertingModels "github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -31,3 +34,18 @@ func SilenceToPostableSilence(s models.Silence) *alertingNotify.PostableSilence
Silence: s.Silence,
}
}
func IntegrationToIntegrationConfig(i models.Integration) (alertingModels.IntegrationConfig, error) {
raw, err := json.Marshal(i.Settings)
if err != nil {
return alertingModels.IntegrationConfig{}, err
}
return alertingModels.IntegrationConfig{
UID: i.UID,
Name: i.Name,
Type: string(i.Config.Type()),
DisableResolveMessage: i.DisableResolveMessage,
Settings: raw,
SecureSettings: i.SecureSettings,
}, nil
}

View File

@@ -378,3 +378,29 @@ func EncryptedReceivers(receivers []*definitions.PostableApiReceiver, encryptFn
}
return encrypted, nil
}
// DecryptIntegrationSettings returns a function to decrypt integration settings.
func DecryptIntegrationSettings(ctx context.Context, ss secretService) models.DecryptFn {
return func(value string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return "", err
}
decrypted, err := ss.Decrypt(ctx, decoded)
if err != nil {
return "", err
}
return string(decrypted), nil
}
}
// EncryptIntegrationSettings returns a function to encrypt integration settings.
func EncryptIntegrationSettings(ctx context.Context, ss secretService) models.EncryptFn {
return func(payload string) (string, error) {
encrypted, err := ss.Encrypt(ctx, []byte(payload), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
}
}

View File

@@ -7,6 +7,7 @@ import (
"sync"
"time"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/alerting/notify/nfstatus"
"github.com/prometheus/client_golang/prometheus"
@@ -71,6 +72,7 @@ type Alertmanager interface {
// Receivers
GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error)
TestIntegration(ctx context.Context, receiverName string, integrationConfig models.Integration, alert alertingModels.TestReceiversConfigAlertParams) (alertingModels.IntegrationStatus, error)
TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*TestTemplatesResults, error)
// Lifecycle

View File

@@ -2,7 +2,6 @@ package notifier
import (
"context"
"encoding/base64"
"errors"
"fmt"
"strings"
@@ -133,27 +132,27 @@ func (rs *ReceiverService) loadProvenances(ctx context.Context, orgID int64) (ma
return rs.provisioningStore.GetProvenances(ctx, orgID, (&models.Integration{}).ResourceType())
}
// GetReceiver returns a receiver by name.
// GetReceiver returns a receiver by its UID.
// The receiver's secure settings are decrypted if requested and the user has access to do so.
func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (*models.Receiver, error) {
func (rs *ReceiverService) GetReceiver(ctx context.Context, uid string, decrypt bool, user identity.Requester) (*models.Receiver, error) {
ctx, span := rs.tracer.Start(ctx, "alerting.receivers.get", trace.WithAttributes(
attribute.Int64("query_org_id", q.OrgID),
attribute.String("query_name", q.Name),
attribute.Bool("query_decrypt", q.Decrypt),
attribute.Int64("query_org_id", user.GetOrgID()),
attribute.String("query_uid", uid),
attribute.Bool("query_decrypt", decrypt),
))
defer span.End()
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
revision, err := rs.cfgStore.Get(ctx, user.GetOrgID())
if err != nil {
return nil, err
}
prov, err := rs.loadProvenances(ctx, q.OrgID)
prov, err := rs.loadProvenances(ctx, user.GetOrgID())
if err != nil {
return nil, err
}
rcv, err := revision.GetReceiver(legacy_storage.NameToUid(q.Name), prov)
rcv, err := revision.GetReceiver(uid, prov)
if err != nil {
if errors.Is(err, legacy_storage.ErrReceiverNotFound) && rs.includeImported {
imported := rs.getImportedReceivers(ctx, span, []string{legacy_storage.NameToUid(q.Name)}, revision)
@@ -171,14 +170,14 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
))
auth := rs.authz.AuthorizeReadDecrypted
if !q.Decrypt {
if !decrypt {
auth = rs.authz.AuthorizeRead
}
if err := auth(ctx, user, rcv); err != nil {
return nil, err
}
if q.Decrypt {
if decrypt {
err := rcv.Decrypt(rs.decryptor(ctx))
if err != nil {
rs.log.FromContext(ctx).Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err)
@@ -684,28 +683,12 @@ func (rs *ReceiverService) deleteProvenances(ctx context.Context, orgID int64, i
// decryptor returns a models.DecryptFn that decrypts a secure setting. If decryption fails, the fallback value is used.
func (rs *ReceiverService) decryptor(ctx context.Context) models.DecryptFn {
return func(value string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return "", err
}
decrypted, err := rs.encryptionService.Decrypt(ctx, decoded)
if err != nil {
return "", err
}
return string(decrypted), nil
}
return DecryptIntegrationSettings(ctx, rs.encryptionService)
}
// encryptor creates an encrypt function that delegates to secrets.Service and returns the base64 encoded result.
func (rs *ReceiverService) encryptor(ctx context.Context) models.EncryptFn {
return func(payload string) (string, error) {
s, err := rs.encryptionService.Encrypt(ctx, []byte(payload), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(s), nil
}
return EncryptIntegrationSettings(ctx, rs.encryptionService)
}
// checkOptimisticConcurrency checks if the existing receiver's version matches the desired version.

View File

@@ -50,7 +50,7 @@ func TestIntegrationReceiverService_GetReceiver(t *testing.T) {
t.Run("service gets receiver from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
recv, err := sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
recv, err := sut.GetReceiver(context.Background(), legacy_storage.NameToUid("slack receiver"), false, redactedUser)
require.NoError(t, err)
require.Equal(t, "slack receiver", recv.Name)
require.Len(t, recv.Integrations, 1)
@@ -60,7 +60,7 @@ func TestIntegrationReceiverService_GetReceiver(t *testing.T) {
t.Run("service returns error when receiver does not exist", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "receiver1"), redactedUser)
_, err := sut.GetReceiver(context.Background(), legacy_storage.NameToUid("receiver1"), redactedUser)
require.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
})
@@ -81,7 +81,7 @@ func TestIntegrationReceiverService_GetReceiver(t *testing.T) {
t.Run("falls to only Grafana if cannot read imported receivers", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService, withImportedIncluded, withInvalidExtraConfig)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "receiver1"), redactedUser)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "receiver1"), false, redactedUser)
require.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
_, err = sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
require.NoError(t, err)
@@ -412,8 +412,7 @@ func TestReceiverService_Delete(t *testing.T) {
// Ensure receiver saved to store is correct.
name, err := legacy_storage.UidToName(tc.deleteUID)
require.NoError(t, err)
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: name}
_, err = sut.GetReceiver(context.Background(), q, writer)
_, err = sut.GetReceiver(context.Background(), legacy_storage.NameToUid(name), false, writer)
assert.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
@@ -626,8 +625,7 @@ func TestReceiverService_Create(t *testing.T) {
assert.Equal(t, tc.expectedCreate, *created)
// Ensure receiver saved to store is correct.
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: tc.receiver.Name, Decrypt: true}
stored, err := sut.GetReceiver(context.Background(), q, decryptUser)
stored, err := sut.GetReceiver(context.Background(), legacy_storage.NameToUid(tc.receiver.Name), true, decryptUser)
require.NoError(t, err)
decrypted := models.CopyReceiverWith(tc.expectedCreate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
decrypted.Version = tc.expectedCreate.Version // Version is calculated before decryption.
@@ -931,8 +929,7 @@ func TestReceiverService_Update(t *testing.T) {
assert.Equal(t, tc.expectedUpdate, *updated)
// Ensure receiver saved to store is correct.
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: tc.receiver.Name, Decrypt: true}
stored, err := sut.GetReceiver(context.Background(), q, decryptUser)
stored, err := sut.GetReceiver(context.Background(), legacy_storage.NameToUid(tc.receiver.Name), true, decryptUser)
require.NoError(t, err)
decrypted := models.CopyReceiverWith(tc.expectedUpdate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
decrypted.Version = tc.expectedUpdate.Version // Version is calculated before decryption.
@@ -1185,7 +1182,7 @@ func TestReceiverServiceAC_Read(t *testing.T) {
return false
}
for _, recv := range allReceivers() {
response, err := sut.GetReceiver(context.Background(), singleQ(orgId, recv.Name), usr)
response, err := sut.GetReceiver(context.Background(), legacy_storage.NameToUid(recv.Name), false, usr)
if isVisible(recv.UID) {
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
assert.NotNil(t, response)
@@ -1207,7 +1204,7 @@ func TestReceiverServiceAC_Read(t *testing.T) {
}
sut.authz = ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), true)
for _, recv := range allReceivers() {
response, err := sut.GetReceiver(context.Background(), singleQ(orgId, recv.Name), usr)
response, err := sut.GetReceiver(context.Background(), legacy_storage.NameToUid(recv.Name), false, usr)
if isVisibleInProvisioning(recv.UID) {
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
assert.NotNil(t, response)
@@ -1842,13 +1839,6 @@ func createEncryptedConfig(t *testing.T, secretService secretService, extraConfi
return string(bytes)
}
func singleQ(orgID int64, name string) models.GetReceiverQuery {
return models.GetReceiverQuery{
OrgID: orgID,
Name: name,
}
}
func multiQ(orgID int64, names ...string) models.GetReceiversQuery {
return models.GetReceiversQuery{
OrgID: orgID,

View File

@@ -0,0 +1,112 @@
package notifier
import (
"context"
"fmt"
"slices"
alertingModels "github.com/grafana/alerting/models"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
type AlertmanagerProvider interface {
AlertmanagerFor(orgID int64) (Alertmanager, error)
}
type ReceiverGetter interface {
GetReceiver(ctx context.Context, uid string, decrypt bool, user identity.Requester) (*models.Receiver, error)
}
func NewReceiverTestingSvc(receiverSvc *ReceiverService, amProvider AlertmanagerProvider, encryptionService secretService) *ReceiverTestingSvc {
return &ReceiverTestingSvc{
receiverSvc: receiverSvc,
amProvider: amProvider,
encryptionService: encryptionService,
}
}
type ReceiverTestingSvc struct {
receiverSvc ReceiverGetter
amProvider AlertmanagerProvider
encryptionService secretService
}
type Alert struct {
Labels map[string]string
Annotations map[string]string
}
type IntegrationTestResult alertingModels.IntegrationStatus
func (t *ReceiverTestingSvc) Test(ctx context.Context, user identity.Requester, alert Alert, receiverUID string, integration models.Integration, requiredSecrets []string) (IntegrationTestResult, error) {
alertParam, err := convertToAlertParam(alert)
if err != nil {
return IntegrationTestResult{}, err
}
decryptedPatchedIntegration, err := t.patchSecrets(ctx, user, receiverUID, integration, requiredSecrets)
if err != nil {
return IntegrationTestResult{}, err
}
err = decryptedPatchedIntegration.Validate(DecryptIntegrationSettings(ctx, t.encryptionService))
if err != nil {
return IntegrationTestResult{}, err
}
am, err := t.amProvider.AlertmanagerFor(user.GetOrgID())
if err != nil {
return IntegrationTestResult{}, err
}
result, err := am.TestIntegration(ctx, "test-receiver", decryptedPatchedIntegration, alertParam)
return IntegrationTestResult(result), err
}
func (t *ReceiverTestingSvc) patchSecrets(ctx context.Context, user identity.Requester, receiverUID string, integration models.Integration, secrets []string) (models.Integration, error) {
if len(secrets) == 0 {
return integration, nil
}
if integration.UID == "" || receiverUID == "" {
return integration, fmt.Errorf("cannot patch secrets for integration without receiver or integration UID")
}
rcv, err := t.receiverSvc.GetReceiver(ctx, receiverUID, false, user)
if err != nil {
return integration, err
}
if rcv == nil {
return integration, fmt.Errorf("cannot patch secrets for receiver that does not exist")
}
idx := slices.IndexFunc(rcv.Integrations, func(i *models.Integration) bool {
return i.UID == integration.UID
})
if idx < 0 {
return integration, fmt.Errorf("cannot patch secrets for integration that does not exist")
}
integration.WithExistingSecureFields(rcv.Integrations[idx], secrets)
err = integration.Decrypt(DecryptIntegrationSettings(ctx, t.encryptionService))
if err != nil {
return integration, err
}
return integration, nil
}
func convertToAlertParam(alert Alert) (alertingModels.TestReceiversConfigAlertParams, error) {
alertParam := alertingModels.TestReceiversConfigAlertParams{
Annotations: make(model.LabelSet, len(alert.Annotations)),
Labels: make(model.LabelSet, len(alert.Labels)),
}
for k, v := range alert.Annotations {
alertParam.Annotations[model.LabelName(k)] = model.LabelValue(v)
}
for k, v := range alert.Labels {
alertParam.Labels[model.LabelName(k)] = model.LabelValue(v)
}
if err := alertParam.Annotations.Validate(); err != nil {
return alertingModels.TestReceiversConfigAlertParams{}, fmt.Errorf("invalid annotations: %w", err)
}
if err := alertParam.Labels.Validate(); err != nil {
return alertingModels.TestReceiversConfigAlertParams{}, fmt.Errorf("invalid labels: %w", err)
}
return alertParam, nil
}

View File

@@ -9,6 +9,7 @@ import (
v2 "github.com/prometheus/alertmanager/api/v2"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) {
@@ -52,6 +53,14 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
})
}
func (am *alertmanager) TestIntegration(ctx context.Context, receiverName string, integrationConfig ngmodels.Integration, alert models.TestReceiversConfigAlertParams) (models.IntegrationStatus, error) {
cfg, err := IntegrationToIntegrationConfig(integrationConfig)
if err != nil {
return models.IntegrationStatus{}, err
}
return am.Base.TestIntegration(ctx, receiverName, cfg, alert)
}
func (am *alertmanager) GetReceivers(_ context.Context) ([]apimodels.Receiver, error) {
return am.Base.GetReceiversStatus(), nil
}

View File

@@ -30,7 +30,8 @@ import (
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/client_golang/prometheus"
common_config "github.com/prometheus/common/config"
"go.yaml.in/yaml/v3"
"github.com/prometheus/common/model"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -40,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
remoteClient "github.com/grafana/grafana/pkg/services/ngalert/remote/client"
"github.com/grafana/grafana/pkg/services/ngalert/sender"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/util/cmputil"
)
@@ -57,6 +59,7 @@ func NoopAutogenFn(_ context.Context, _ log.Logger, _ int64, _ *apimodels.Postab
}
type Crypto interface {
Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error)
Decrypt(ctx context.Context, payload []byte) ([]byte, error)
DecryptExtraConfigs(ctx context.Context, config *apimodels.PostableUserConfig) error
}
@@ -289,20 +292,6 @@ func (am *Alertmanager) isDefaultConfiguration(configHash string) bool {
return configHash == am.defaultConfigHash
}
func decrypter(ctx context.Context, crypto Crypto) models.DecryptFn {
return func(value string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return "", err
}
decrypted, err := crypto.Decrypt(ctx, decoded)
if err != nil {
return "", err
}
return string(decrypted), nil
}
}
// buildConfiguration takes a raw Alertmanager configuration and returns a config that the remote Alertmanager can use.
// It parses the initial configuration, adds auto-generated routes, decrypts receivers, and merges the extra configs.
func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, createdAtEpoch int64, autogenInvalidReceiverAction notifier.InvalidReceiversAction) (remoteClient.UserGrafanaConfig, error) {
@@ -317,7 +306,7 @@ func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, crea
}
// Decrypt the receivers in the configuration.
decryptedReceivers, err := notifier.DecryptedReceivers(c.AlertmanagerConfig.Receivers, decrypter(ctx, am.crypto))
decryptedReceivers, err := notifier.DecryptedReceivers(c.AlertmanagerConfig.Receivers, notifier.DecryptIntegrationSettings(ctx, am.crypto))
if err != nil {
return remoteClient.UserGrafanaConfig{}, fmt.Errorf("unable to decrypt receivers: %w", err)
}
@@ -619,7 +608,7 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver,
}
func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) {
decryptedReceivers, err := notifier.DecryptedReceivers(c.Receivers, decrypter(ctx, am.crypto))
decryptedReceivers, err := notifier.DecryptedReceivers(c.Receivers, notifier.DecryptIntegrationSettings(ctx, am.crypto))
if err != nil {
return nil, 0, fmt.Errorf("failed to decrypt receivers: %w", err)
}
@@ -636,6 +625,51 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
})
}
func (am *Alertmanager) TestIntegration(ctx context.Context, receiverName string, integrationConfig models.Integration, alert alertingModels.TestReceiversConfigAlertParams) (alertingModels.IntegrationStatus, error) {
decrypted := integrationConfig.Clone()
err := decrypted.Decrypt(notifier.DecryptIntegrationSettings(ctx, am.crypto))
if err != nil {
return alertingModels.IntegrationStatus{}, fmt.Errorf("failed to decrypt receivers: %w", err)
}
cfg, err := notifier.IntegrationToIntegrationConfig(decrypted)
if err != nil {
return alertingModels.IntegrationStatus{}, fmt.Errorf("failed to convert integration to integration config: %w", err)
}
apiReceivers := []*alertingNotify.APIReceiver{
{
ConfigReceiver: alertingNotify.ConfigReceiver{
Name: receiverName,
},
ReceiverConfig: alertingModels.ReceiverConfig{
Integrations: []*alertingModels.IntegrationConfig{
&cfg,
},
},
},
}
t := time.Now()
result, _, err := am.mimirClient.TestReceivers(ctx, alertingNotify.TestReceiversConfigBodyParams{
Alert: &alert,
Receivers: apiReceivers,
})
duration := time.Since(t)
if err != nil {
return alertingModels.IntegrationStatus{}, fmt.Errorf("failed to test integration: %w", err)
}
status := alertingModels.IntegrationStatus{
LastNotifyAttempt: strfmt.DateTime(result.NotifedAt),
LastNotifyAttemptDuration: model.Duration(duration).String(),
Name: cfg.Type,
SendResolved: false,
}
if len(result.Receivers) > 0 && len(result.Receivers[0].Configs) > 0 {
status.LastNotifyAttemptError = result.Receivers[0].Configs[0].Error
}
return status, nil
}
func (am *Alertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
for _, alert := range c.Alerts {
notifier.AddDefaultLabelsAndAnnotations(alert)

View File

@@ -43,7 +43,6 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/remote/client"
ngfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
@@ -298,13 +297,7 @@ func TestIntegrationApplyConfig(t *testing.T) {
var c apimodels.PostableUserConfig
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &c))
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
encryptedReceivers, err := notifier.EncryptedReceivers(c.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
})
encryptedReceivers, err := notifier.EncryptedReceivers(c.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
c.AlertmanagerConfig.Receivers = encryptedReceivers
require.NoError(t, err)
@@ -462,13 +455,7 @@ func TestCompareAndSendConfiguration(t *testing.T) {
// Create a config with correctly encrypted and encoded secrets.
var inputCfg apimodels.PostableUserConfig
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
})
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
require.NoError(t, err)
testGrafanaConfigWithEncryptedSecret, err := json.Marshal(inputCfg)
@@ -663,13 +650,7 @@ func Test_TestReceiversDecryptsSecureSettings(t *testing.T) {
var inputCfg apimodels.PostableUserConfig
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
})
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
require.NoError(t, err)
@@ -1037,13 +1018,7 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) {
{
postableCfg, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
require.NoError(t, err)
encryptedReceivers, err := notifier.EncryptedReceivers(postableCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
})
encryptedReceivers, err := notifier.EncryptedReceivers(postableCfg.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
postableCfg.AlertmanagerConfig.Receivers = encryptedReceivers
require.NoError(t, err)

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
alertingModels "github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/infra/kvstore"
@@ -173,6 +174,10 @@ func (fam *RemotePrimaryForkedAlertmanager) TestReceivers(ctx context.Context, c
return fam.remote.TestReceivers(ctx, c)
}
func (fam *RemotePrimaryForkedAlertmanager) TestIntegration(ctx context.Context, receiverName string, integrationConfig models.Integration, alert alertingModels.TestReceiversConfigAlertParams) (alertingModels.IntegrationStatus, error) {
return fam.remote.TestIntegration(ctx, receiverName, integrationConfig, alert)
}
func (fam *RemotePrimaryForkedAlertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
return fam.remote.TestTemplate(ctx, c)
}

View File

@@ -6,6 +6,7 @@ import (
"sync"
"time"
alertingModels "github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/infra/kvstore"
@@ -234,6 +235,10 @@ func (fam *RemoteSecondaryForkedAlertmanager) TestReceivers(ctx context.Context,
return fam.internal.TestReceivers(ctx, c)
}
func (fam *RemoteSecondaryForkedAlertmanager) TestIntegration(ctx context.Context, receiverName string, integrationConfig models.Integration, alert alertingModels.TestReceiversConfigAlertParams) (alertingModels.IntegrationStatus, error) {
return fam.internal.TestIntegration(ctx, receiverName, integrationConfig, alert)
}
func (fam *RemoteSecondaryForkedAlertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
return fam.internal.TestTemplate(ctx, c)
}