Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4236845ea8 | |||
| d4e94cef50 | |||
| c723526f4e | |||
| e56fc80d93 | |||
| dfaa5ec1d4 | |||
| 4abd88ec95 | |||
| 405871d41d |
@@ -430,4 +430,100 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
scope: Namespaced
|
scope: Namespaced
|
||||||
name: v0alpha1
|
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
|
served: true
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
package kinds
|
package kinds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time",
|
||||||
|
"github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
#Alert: {
|
||||||
|
labels: {
|
||||||
|
[string]: string
|
||||||
|
}
|
||||||
|
annotations: {
|
||||||
|
[string]: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
manifest: {
|
manifest: {
|
||||||
appName: "alerting-notifications"
|
appName: "alerting-notifications"
|
||||||
groupOverride: "notifications.alerting.grafana.app"
|
groupOverride: "notifications.alerting.grafana.app"
|
||||||
@@ -14,7 +28,28 @@ manifest: {
|
|||||||
routeTreev0alpha1,
|
routeTreev0alpha1,
|
||||||
templatev0alpha1,
|
templatev0alpha1,
|
||||||
timeIntervalv0alpha1,
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -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{}
|
||||||
|
}
|
||||||
+37
@@ -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{
|
Routes: app.ManifestVersionRoutes{
|
||||||
Namespaced: map[string]spec3.PathProps{},
|
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{},
|
Cluster: map[string]spec3.PathProps{},
|
||||||
Schemas: map[string]spec.Schema{},
|
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
|
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.
|
// 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.
|
// 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
|
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) {
|
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
|
||||||
if len(path) > 0 && path[0] == '/' {
|
if len(path) > 0 && path[0] == '/' {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana-app-sdk/app"
|
"github.com/grafana/grafana-app-sdk/app"
|
||||||
"github.com/grafana/grafana-app-sdk/logging"
|
"github.com/grafana/grafana-app-sdk/logging"
|
||||||
@@ -9,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana-app-sdk/simple"
|
"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"
|
||||||
|
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(cfg app.Config) (app.App, error) {
|
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{
|
c := simple.AppConfig{
|
||||||
Name: "alerting.notification",
|
Name: "alerting.notification",
|
||||||
KubeConfig: cfg.KubeConfig,
|
KubeConfig: cfg.KubeConfig,
|
||||||
@@ -30,6 +41,15 @@ func New(cfg app.Config) (app.App, error) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ManagedKinds: managedKinds,
|
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)
|
a, err := simple.NewApp(c)
|
||||||
|
|||||||
@@ -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,23 +128,35 @@ func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[str
|
|||||||
}
|
}
|
||||||
storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations))
|
storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations))
|
||||||
for _, integration := range 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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
domain.Integrations = append(domain.Integrations, &grafanaIntegration)
|
||||||
|
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
|
var config schema.IntegrationSchemaVersion
|
||||||
typeSchema, _ := alertingNotify.GetSchemaForIntegration(t)
|
typeSchema, _ := alertingNotify.GetSchemaForIntegration(t)
|
||||||
if integration.Version != "" {
|
if integration.Version != "" {
|
||||||
var ok bool
|
var ok bool
|
||||||
config, ok = typeSchema.GetVersion(schema.Version(integration.Version))
|
config, ok = typeSchema.GetVersion(schema.Version(integration.Version))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, fmt.Errorf("invalid version %s for integration type %s", integration.Version, integration.Type)
|
return ngmodels.Integration{}, nil, fmt.Errorf("invalid version %s for integration type %s", integration.Version, integration.Type)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
config = typeSchema.GetCurrentVersion()
|
config = typeSchema.GetCurrentVersion()
|
||||||
}
|
}
|
||||||
grafanaIntegration := ngmodels.Integration{
|
grafanaIntegration := ngmodels.Integration{
|
||||||
Name: receiver.Spec.Title,
|
Name: receiverTitle,
|
||||||
Config: config,
|
Config: config,
|
||||||
Settings: maps.Clone(integration.Settings),
|
Settings: maps.Clone(integration.Settings),
|
||||||
SecureSettings: make(map[string]string),
|
SecureSettings: make(map[string]string),
|
||||||
@@ -156,19 +168,15 @@ func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[str
|
|||||||
grafanaIntegration.DisableResolveMessage = *integration.DisableResolveMessage
|
grafanaIntegration.DisableResolveMessage = *integration.DisableResolveMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
domain.Integrations = append(domain.Integrations, &grafanaIntegration)
|
var secureFields []string
|
||||||
|
|
||||||
if grafanaIntegration.UID != "" {
|
if grafanaIntegration.UID != "" {
|
||||||
// This is an existing integration, so we track the secure fields being requested to copy over from existing values.
|
// 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))
|
secureFields = make([]string, 0, len(integration.SecureFields))
|
||||||
for k, isSecure := range integration.SecureFields {
|
for k, isSecure := range integration.SecureFields {
|
||||||
if isSecure {
|
if isSecure {
|
||||||
secureFields = append(secureFields, k)
|
secureFields = append(secureFields, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storedSecureFields[grafanaIntegration.UID] = secureFields
|
|
||||||
}
|
}
|
||||||
}
|
return grafanaIntegration, secureFields, nil
|
||||||
|
|
||||||
return domain, storedSecureFields, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ReceiverService interface {
|
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)
|
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)
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, apierrors.NewNotFound(ResourceInfo.GroupResource(), uid)
|
return nil, apierrors.NewNotFound(ResourceInfo.GroupResource(), uid)
|
||||||
}
|
}
|
||||||
q := ngmodels.GetReceiverQuery{
|
|
||||||
OrgID: info.OrgID,
|
|
||||||
Name: name,
|
|
||||||
Decrypt: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := identity.GetRequester(ctx)
|
user, err := identity.GetRequester(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := s.service.GetReceiver(ctx, q, user)
|
r, err := s.service.GetReceiver(ctx, name, false, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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"
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"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"
|
||||||
|
"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/routingtree"
|
||||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/templategroup"
|
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/templategroup"
|
||||||
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval"
|
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval"
|
||||||
@@ -53,6 +54,10 @@ func RegisterAppInstaller(
|
|||||||
ng: ng,
|
ng: ng,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customCfg := notificationsApp.Config{
|
||||||
|
ReceiverTestingHandler: receivertesting.New(ng),
|
||||||
|
}
|
||||||
|
|
||||||
localManifest := apis.LocalManifest()
|
localManifest := apis.LocalManifest()
|
||||||
|
|
||||||
provider := simple.NewAppProvider(localManifest, nil, notificationsApp.New)
|
provider := simple.NewAppProvider(localManifest, nil, notificationsApp.New)
|
||||||
@@ -60,7 +65,7 @@ func RegisterAppInstaller(
|
|||||||
appConfig := app.Config{
|
appConfig := app.Config{
|
||||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||||
ManifestData: *localManifest.ManifestData,
|
ManifestData: *localManifest.ManifestData,
|
||||||
SpecificConfig: nil,
|
SpecificConfig: &customCfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{})
|
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)
|
return receiver.Authorize(ctx, ac.NewReceiverAccess[*ngmodels.Receiver](authz, false), a)
|
||||||
case routingtree.ResourceInfo.GroupResource().Resource:
|
case routingtree.ResourceInfo.GroupResource().Resource:
|
||||||
return routingtree.Authorize(ctx, authz, a)
|
return routingtree.Authorize(ctx, authz, a)
|
||||||
|
case "testing":
|
||||||
|
return receivertesting.Authorize(ctx, authz, a)
|
||||||
}
|
}
|
||||||
return authorizer.DecisionNoOpinion, "", nil
|
return authorizer.DecisionNoOpinion, "", nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -813,6 +813,65 @@ func (_c *AlertmanagerMock_StopAndWait_Call) RunAndReturn(run func()) *Alertmana
|
|||||||
return _c
|
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
|
// TestReceivers provides a mock function with given fields: ctx, c
|
||||||
func (_m *AlertmanagerMock) TestReceivers(ctx context.Context, c definitions.TestReceiversConfigBodyParams) (*notify.TestReceiversResult, int, error) {
|
func (_m *AlertmanagerMock) TestReceivers(ctx context.Context, c definitions.TestReceiversConfigBodyParams) (*notify.TestReceiversResult, int, error) {
|
||||||
ret := _m.Called(ctx, c)
|
ret := _m.Called(ctx, c)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package notifier
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
alertingModels "github.com/grafana/alerting/models"
|
||||||
alertingNotify "github.com/grafana/alerting/notify"
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
@@ -31,3 +34,18 @@ func SilenceToPostableSilence(s models.Silence) *alertingNotify.PostableSilence
|
|||||||
Silence: s.Silence,
|
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
|
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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
alertingModels "github.com/grafana/alerting/models"
|
||||||
"github.com/grafana/alerting/notify/nfstatus"
|
"github.com/grafana/alerting/notify/nfstatus"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ type Alertmanager interface {
|
|||||||
// Receivers
|
// Receivers
|
||||||
GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
|
GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
|
||||||
TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, 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)
|
TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*TestTemplatesResults, error)
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package notifier
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -133,27 +132,27 @@ func (rs *ReceiverService) loadProvenances(ctx context.Context, orgID int64) (ma
|
|||||||
return rs.provisioningStore.GetProvenances(ctx, orgID, (&models.Integration{}).ResourceType())
|
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.
|
// 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(
|
ctx, span := rs.tracer.Start(ctx, "alerting.receivers.get", trace.WithAttributes(
|
||||||
attribute.Int64("query_org_id", q.OrgID),
|
attribute.Int64("query_org_id", user.GetOrgID()),
|
||||||
attribute.String("query_name", q.Name),
|
attribute.String("query_uid", uid),
|
||||||
attribute.Bool("query_decrypt", q.Decrypt),
|
attribute.Bool("query_decrypt", decrypt),
|
||||||
))
|
))
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
|
revision, err := rs.cfgStore.Get(ctx, user.GetOrgID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prov, err := rs.loadProvenances(ctx, q.OrgID)
|
prov, err := rs.loadProvenances(ctx, user.GetOrgID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rcv, err := revision.GetReceiver(legacy_storage.NameToUid(q.Name), prov)
|
rcv, err := revision.GetReceiver(uid, prov)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, legacy_storage.ErrReceiverNotFound) && rs.includeImported {
|
if errors.Is(err, legacy_storage.ErrReceiverNotFound) && rs.includeImported {
|
||||||
imported := rs.getImportedReceivers(ctx, span, []string{legacy_storage.NameToUid(q.Name)}, revision)
|
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
|
auth := rs.authz.AuthorizeReadDecrypted
|
||||||
if !q.Decrypt {
|
if !decrypt {
|
||||||
auth = rs.authz.AuthorizeRead
|
auth = rs.authz.AuthorizeRead
|
||||||
}
|
}
|
||||||
if err := auth(ctx, user, rcv); err != nil {
|
if err := auth(ctx, user, rcv); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.Decrypt {
|
if decrypt {
|
||||||
err := rcv.Decrypt(rs.decryptor(ctx))
|
err := rcv.Decrypt(rs.decryptor(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rs.log.FromContext(ctx).Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err)
|
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.
|
// 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 {
|
func (rs *ReceiverService) decryptor(ctx context.Context) models.DecryptFn {
|
||||||
return func(value string) (string, error) {
|
return DecryptIntegrationSettings(ctx, rs.encryptionService)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// encryptor creates an encrypt function that delegates to secrets.Service and returns the base64 encoded result.
|
// 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 {
|
func (rs *ReceiverService) encryptor(ctx context.Context) models.EncryptFn {
|
||||||
return func(payload string) (string, error) {
|
return EncryptIntegrationSettings(ctx, rs.encryptionService)
|
||||||
s, err := rs.encryptionService.Encrypt(ctx, []byte(payload), secrets.WithoutScope())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(s), nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkOptimisticConcurrency checks if the existing receiver's version matches the desired version.
|
// 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) {
|
t.Run("service gets receiver from AM config", func(t *testing.T) {
|
||||||
sut := createReceiverServiceSut(t, secretsService)
|
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.NoError(t, err)
|
||||||
require.Equal(t, "slack receiver", recv.Name)
|
require.Equal(t, "slack receiver", recv.Name)
|
||||||
require.Len(t, recv.Integrations, 1)
|
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) {
|
t.Run("service returns error when receiver does not exist", func(t *testing.T) {
|
||||||
sut := createReceiverServiceSut(t, secretsService)
|
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)
|
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) {
|
t.Run("falls to only Grafana if cannot read imported receivers", func(t *testing.T) {
|
||||||
sut := createReceiverServiceSut(t, secretsService, withImportedIncluded, withInvalidExtraConfig)
|
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)
|
require.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
|
||||||
_, err = sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
|
_, err = sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -412,8 +412,7 @@ func TestReceiverService_Delete(t *testing.T) {
|
|||||||
// Ensure receiver saved to store is correct.
|
// Ensure receiver saved to store is correct.
|
||||||
name, err := legacy_storage.UidToName(tc.deleteUID)
|
name, err := legacy_storage.UidToName(tc.deleteUID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: name}
|
_, err = sut.GetReceiver(context.Background(), legacy_storage.NameToUid(name), false, writer)
|
||||||
_, err = sut.GetReceiver(context.Background(), q, writer)
|
|
||||||
assert.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
|
assert.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
|
||||||
|
|
||||||
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
|
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)
|
assert.Equal(t, tc.expectedCreate, *created)
|
||||||
|
|
||||||
// Ensure receiver saved to store is correct.
|
// 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(), legacy_storage.NameToUid(tc.receiver.Name), true, decryptUser)
|
||||||
stored, err := sut.GetReceiver(context.Background(), q, decryptUser)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
decrypted := models.CopyReceiverWith(tc.expectedCreate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
|
decrypted := models.CopyReceiverWith(tc.expectedCreate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
|
||||||
decrypted.Version = tc.expectedCreate.Version // Version is calculated before decryption.
|
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)
|
assert.Equal(t, tc.expectedUpdate, *updated)
|
||||||
|
|
||||||
// Ensure receiver saved to store is correct.
|
// 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(), legacy_storage.NameToUid(tc.receiver.Name), true, decryptUser)
|
||||||
stored, err := sut.GetReceiver(context.Background(), q, decryptUser)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
decrypted := models.CopyReceiverWith(tc.expectedUpdate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
|
decrypted := models.CopyReceiverWith(tc.expectedUpdate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
|
||||||
decrypted.Version = tc.expectedUpdate.Version // Version is calculated before decryption.
|
decrypted.Version = tc.expectedUpdate.Version // Version is calculated before decryption.
|
||||||
@@ -1185,7 +1182,7 @@ func TestReceiverServiceAC_Read(t *testing.T) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, recv := range allReceivers() {
|
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) {
|
if isVisible(recv.UID) {
|
||||||
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
|
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
|
||||||
assert.NotNil(t, response)
|
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)
|
sut.authz = ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), true)
|
||||||
for _, recv := range allReceivers() {
|
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) {
|
if isVisibleInProvisioning(recv.UID) {
|
||||||
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
|
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
|
||||||
assert.NotNil(t, response)
|
assert.NotNil(t, response)
|
||||||
@@ -1842,13 +1839,6 @@ func createEncryptedConfig(t *testing.T, secretService secretService, extraConfi
|
|||||||
return string(bytes)
|
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 {
|
func multiQ(orgID int64, names ...string) models.GetReceiversQuery {
|
||||||
return models.GetReceiversQuery{
|
return models.GetReceiversQuery{
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
|
|||||||
@@ -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"
|
v2 "github.com/prometheus/alertmanager/api/v2"
|
||||||
|
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
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) {
|
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) {
|
func (am *alertmanager) GetReceivers(_ context.Context) ([]apimodels.Receiver, error) {
|
||||||
return am.Base.GetReceiversStatus(), nil
|
return am.Base.GetReceiversStatus(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import (
|
|||||||
"github.com/prometheus/alertmanager/pkg/labels"
|
"github.com/prometheus/alertmanager/pkg/labels"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
common_config "github.com/prometheus/common/config"
|
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/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
@@ -40,6 +41,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||||
remoteClient "github.com/grafana/grafana/pkg/services/ngalert/remote/client"
|
remoteClient "github.com/grafana/grafana/pkg/services/ngalert/remote/client"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/sender"
|
"github.com/grafana/grafana/pkg/services/ngalert/sender"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
"github.com/grafana/grafana/pkg/util/cmputil"
|
"github.com/grafana/grafana/pkg/util/cmputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ func NoopAutogenFn(_ context.Context, _ log.Logger, _ int64, _ *apimodels.Postab
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Crypto interface {
|
type Crypto interface {
|
||||||
|
Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error)
|
||||||
Decrypt(ctx context.Context, payload []byte) ([]byte, error)
|
Decrypt(ctx context.Context, payload []byte) ([]byte, error)
|
||||||
DecryptExtraConfigs(ctx context.Context, config *apimodels.PostableUserConfig) error
|
DecryptExtraConfigs(ctx context.Context, config *apimodels.PostableUserConfig) error
|
||||||
}
|
}
|
||||||
@@ -289,20 +292,6 @@ func (am *Alertmanager) isDefaultConfiguration(configHash string) bool {
|
|||||||
return configHash == am.defaultConfigHash
|
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.
|
// 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.
|
// 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) {
|
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.
|
// 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 {
|
if err != nil {
|
||||||
return remoteClient.UserGrafanaConfig{}, fmt.Errorf("unable to decrypt receivers: %w", err)
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("failed to decrypt receivers: %w", err)
|
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) {
|
func (am *Alertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
|
||||||
for _, alert := range c.Alerts {
|
for _, alert := range c.Alerts {
|
||||||
notifier.AddDefaultLabelsAndAnnotations(alert)
|
notifier.AddDefaultLabelsAndAnnotations(alert)
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/remote/client"
|
"github.com/grafana/grafana/pkg/services/ngalert/remote/client"
|
||||||
ngfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
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/database"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
@@ -298,13 +297,7 @@ func TestIntegrationApplyConfig(t *testing.T) {
|
|||||||
var c apimodels.PostableUserConfig
|
var c apimodels.PostableUserConfig
|
||||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &c))
|
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &c))
|
||||||
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
|
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
|
||||||
encryptedReceivers, err := notifier.EncryptedReceivers(c.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
encryptedReceivers, err := notifier.EncryptedReceivers(c.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
|
||||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
||||||
})
|
|
||||||
c.AlertmanagerConfig.Receivers = encryptedReceivers
|
c.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -462,13 +455,7 @@ func TestCompareAndSendConfiguration(t *testing.T) {
|
|||||||
// Create a config with correctly encrypted and encoded secrets.
|
// Create a config with correctly encrypted and encoded secrets.
|
||||||
var inputCfg apimodels.PostableUserConfig
|
var inputCfg apimodels.PostableUserConfig
|
||||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
|
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
|
||||||
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
|
||||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
||||||
})
|
|
||||||
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testGrafanaConfigWithEncryptedSecret, err := json.Marshal(inputCfg)
|
testGrafanaConfigWithEncryptedSecret, err := json.Marshal(inputCfg)
|
||||||
@@ -663,13 +650,7 @@ func Test_TestReceiversDecryptsSecureSettings(t *testing.T) {
|
|||||||
|
|
||||||
var inputCfg apimodels.PostableUserConfig
|
var inputCfg apimodels.PostableUserConfig
|
||||||
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
|
require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &inputCfg))
|
||||||
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
encryptedReceivers, err := notifier.EncryptedReceivers(inputCfg.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
|
||||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
||||||
})
|
|
||||||
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
inputCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1037,13 +1018,7 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) {
|
|||||||
{
|
{
|
||||||
postableCfg, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
|
postableCfg, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
encryptedReceivers, err := notifier.EncryptedReceivers(postableCfg.AlertmanagerConfig.Receivers, func(payload string) (string, error) {
|
encryptedReceivers, err := notifier.EncryptedReceivers(postableCfg.AlertmanagerConfig.Receivers, notifier.EncryptIntegrationSettings(context.Background(), secretsService))
|
||||||
encrypted, err := secretsService.Encrypt(context.Background(), []byte(payload), secrets.WithoutScope())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
||||||
})
|
|
||||||
postableCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
postableCfg.AlertmanagerConfig.Receivers = encryptedReceivers
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
alertingModels "github.com/grafana/alerting/models"
|
||||||
alertingNotify "github.com/grafana/alerting/notify"
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
"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)
|
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) {
|
func (fam *RemotePrimaryForkedAlertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
|
||||||
return fam.remote.TestTemplate(ctx, c)
|
return fam.remote.TestTemplate(ctx, c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
alertingModels "github.com/grafana/alerting/models"
|
||||||
alertingNotify "github.com/grafana/alerting/notify"
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
@@ -234,6 +235,10 @@ func (fam *RemoteSecondaryForkedAlertmanager) TestReceivers(ctx context.Context,
|
|||||||
return fam.internal.TestReceivers(ctx, c)
|
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) {
|
func (fam *RemoteSecondaryForkedAlertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error) {
|
||||||
return fam.internal.TestTemplate(ctx, c)
|
return fam.internal.TestTemplate(ctx, c)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user