mirror of
https://github.com/grafana/grafana.git
synced 2026-01-10 05:57:40 +08:00
Compare commits
7 Commits
sriram/SQL
...
yuri-tcere
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4236845ea8 | ||
|
|
d4e94cef50 | ||
|
|
c723526f4e | ||
|
|
e56fc80d93 | ||
|
|
dfaa5ec1d4 | ||
|
|
4abd88ec95 | ||
|
|
405871d41d |
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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] == '/' {
|
||||
|
||||
@@ -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)
|
||||
|
||||
23
apps/alerting/notifications/pkg/app/config.go
Normal file
23
apps/alerting/notifications/pkg/app/config.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
112
pkg/services/ngalert/notifier/receiver_testing_svc.go
Normal file
112
pkg/services/ngalert/notifier/receiver_testing_svc.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user