From ff9eff49bdfc58232de168fea9f348be43d7c610 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 21 Jun 2023 20:53:30 -0300 Subject: [PATCH] Alerting: Bump grafana/alerting and refactor the ImageStore/Provider to provide image URL/bytes (#70182) * implement alerting.images.Provider interface in our ImageStore * add URLExists() method to fakeConfigStore * make linter happy * update integration tests --- go.mod | 10 +- go.sum | 10 +- pkg/services/ngalert/notifier/alertmanager.go | 2 +- pkg/services/ngalert/notifier/email_test.go | 4 +- pkg/services/ngalert/notifier/images.go | 151 +++++++++++++-- pkg/services/ngalert/notifier/images_test.go | 180 +++++++++++++++++- pkg/services/ngalert/notifier/testing.go | 14 +- pkg/services/ngalert/store/image.go | 17 ++ pkg/services/ngalert/store/testing.go | 20 +- .../sqlstore/migrations/ualert/tables.go | 1 + .../alerting/api_notification_channel_test.go | 38 ++-- 11 files changed, 388 insertions(+), 59 deletions(-) diff --git a/go.mod b/go.mod index 788cac7d025..5bb4791775a 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 github.com/gorilla/websocket v1.5.0 - github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba + github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890 github.com/grafana/cuetsy v0.1.9 github.com/grafana/grafana-aws-sdk v0.15.0 github.com/grafana/grafana-azure-sdk-go v1.7.0 @@ -91,7 +91,7 @@ require ( github.com/prometheus/prometheus v1.8.2-0.20210621150501-ff58416a0b02 github.com/robfig/cron/v3 v3.0.1 github.com/russellhaering/goxmldsig v1.2.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f github.com/uber/jaeger-client-go v2.29.1+incompatible // indirect @@ -110,7 +110,7 @@ require ( golang.org/x/exp v0.0.0-20230307190834-24139beb5833 golang.org/x/net v0.9.0 golang.org/x/oauth2 v0.6.0 - golang.org/x/sync v0.1.0 + golang.org/x/sync v0.3.0 golang.org/x/time v0.3.0 golang.org/x/tools v0.7.0 gonum.org/v1/gonum v0.11.0 @@ -122,7 +122,7 @@ require ( gopkg.in/mail.v2 v2.3.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 - xorm.io/builder v0.3.6 + xorm.io/builder v0.3.6 // indirect xorm.io/core v0.7.3 xorm.io/xorm v0.8.2 ) @@ -418,7 +418,7 @@ require ( github.com/xlab/treeprint v1.1.0 go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - golang.org/x/mod v0.9.0 // indirect + golang.org/x/mod v0.9.0 gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index ab7459e49ba..3c7ed2897e6 100644 --- a/go.sum +++ b/go.sum @@ -1356,8 +1356,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba h1:aNTu22ojw4XY24DYNAuvw8v/5iFUNk2bkdTeeWQ5+0o= -github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o= +github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890 h1:ubNIgVGX4PQ9YI1nWnt2mky3il8clWSjdo3NFSD26DQ= +github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890/go.mod h1:zEflOvMVchYhRbFb5ziXVR/JG67FOLBzQTjhHh9xaI4= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cuetsy v0.1.9 h1:EwT8BqHoC0B3B4Y6Lg/D1aeYUKnQC1+2jjOHNsOuUfU= @@ -2380,8 +2380,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= @@ -2941,8 +2942,9 @@ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 73584b0ec81..5eb4bcfcaa8 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -359,7 +359,7 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *alertingNotify.APIRe return nil, err } s := &sender{am.NotificationService} - img := newImageStore(am.Store) + img := newImageProvider(am.Store, log.New("ngalert.notifier.image-provider")) integrations, err := alertingNotify.BuildReceiverIntegrations( receiverCfg, tmpl, diff --git a/pkg/services/ngalert/notifier/email_test.go b/pkg/services/ngalert/notifier/email_test.go index 01da729b9bd..391fba8eac1 100644 --- a/pkg/services/ngalert/notifier/email_test.go +++ b/pkg/services/ngalert/notifier/email_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/grafana/alerting/images" + alertingImages "github.com/grafana/alerting/images" alertingLogging "github.com/grafana/alerting/logging" "github.com/grafana/alerting/receivers" alertingEmail "github.com/grafana/alerting/receivers/email" @@ -200,7 +200,7 @@ func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl * }, Message: messageTmpl, Subject: subjectTmpl, - }, receivers.Metadata{}, emailTmpl, ns, &images.UnavailableImageStore{}, &alertingLogging.FakeLogger{}) + }, receivers.Metadata{}, emailTmpl, ns, &alertingImages.UnavailableProvider{}, &alertingLogging.FakeLogger{}) } func getSingleSentMessage(t *testing.T, ns *emailSender) *notifications.Message { diff --git a/pkg/services/ngalert/notifier/images.go b/pkg/services/ngalert/notifier/images.go index 10a29b00663..5ddbf34e64c 100644 --- a/pkg/services/ngalert/notifier/images.go +++ b/pkg/services/ngalert/notifier/images.go @@ -2,45 +2,154 @@ package notifier import ( "context" + "errors" + "io" + "os" + "path/filepath" "strings" - "github.com/grafana/alerting/images" - + alertingImages "github.com/grafana/alerting/images" + alertingModels "github.com/grafana/alerting/models" + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" ) -type imageStore struct { - store store.ImageStore +type imageProvider struct { + store store.ImageStore + logger log.Logger } -func newImageStore(store store.ImageStore) images.ImageStore { - return &imageStore{ - store: store, +func newImageProvider(store store.ImageStore, logger log.Logger) alertingImages.Provider { + return &imageProvider{ + store: store, + logger: logger, } } -func (i imageStore) GetImage(ctx context.Context, uri string) (*images.Image, error) { - var ( - image *models.Image - err error - ) - - // Check whether the uri is a URL or a token to know how to query the DB. - if strings.HasPrefix(uri, "http") { - image, err = i.store.GetImageByURL(ctx, uri) - } else { - token := strings.TrimPrefix(uri, "token://") - image, err = i.store.GetImage(ctx, token) - } +func (i imageProvider) GetImage(ctx context.Context, uri string) (*alertingImages.Image, error) { + image, err := i.getImageFromURI(ctx, uri) if err != nil { + if errors.Is(err, models.ErrImageNotFound) { + i.logger.Info("Image not found in database") + return nil, alertingImages.ErrImageNotFound + } return nil, err } - return &images.Image{ + return &alertingImages.Image{ Token: image.Token, Path: image.Path, URL: image.URL, CreatedAt: image.CreatedAt, }, nil } + +func (i imageProvider) GetImageURL(ctx context.Context, alert *alertingNotify.Alert) (string, error) { + uri, err := getImageURI(alert) + if err != nil { + return "", err + } + + // If the identifier is a URL, validate that it corresponds to a stored, non-expired image. + if strings.HasPrefix(uri, "http") { + i.logger.Debug("Received an image URL in annotations", "alert", alert) + exists, err := i.store.URLExists(ctx, uri) + if err != nil { + return "", err + } + if !exists { + i.logger.Info("Image URL not found in database", "alert", alert) + return "", alertingImages.ErrImageNotFound + } + return uri, nil + } + + // If the identifier is a token, remove the prefix, get the image and return the URL. + token := strings.TrimPrefix(uri, "token://") + i.logger.Debug("Received an image token in annotations", "alert", alert, "token", token) + return i.getImageURLFromToken(ctx, token) +} + +// getImageURLFromToken takes a token and returns the URL of the image that token belongs to. +func (i imageProvider) getImageURLFromToken(ctx context.Context, token string) (string, error) { + image, err := i.store.GetImage(ctx, token) + if err != nil { + if errors.Is(err, models.ErrImageNotFound) { + i.logger.Info("Image not found in database", "token", token) + return "", alertingImages.ErrImageNotFound + } + return "", err + } + + if !image.HasURL() { + return "", alertingImages.ErrImagesNoURL + } + return image.URL, nil +} + +func (i imageProvider) GetRawImage(ctx context.Context, alert *alertingNotify.Alert) (io.ReadCloser, string, error) { + uri, err := getImageURI(alert) + if err != nil { + return nil, "", err + } + + image, err := i.getImageFromURI(ctx, uri) + if err != nil { + if errors.Is(err, models.ErrImageNotFound) { + i.logger.Info("Image not found in database", "alert", alert) + return nil, "", alertingImages.ErrImageNotFound + } + return nil, "", err + } + if !image.HasPath() { + return nil, "", alertingImages.ErrImagesNoPath + } + + // Return image bytes and filename. + readCloser, err := openImage(image.Path) + if err != nil { + i.logger.Error("Error looking for image on disk", "alert", alert, "path", image.Path, "error", err) + return nil, "", err + } + filename := filepath.Base(image.Path) + return readCloser, filename, nil +} + +func (i imageProvider) getImageFromURI(ctx context.Context, uri string) (*models.Image, error) { + // Check whether the uri is a URL or a token to know how to query the DB. + if strings.HasPrefix(uri, "http") { + i.logger.Debug("Received an image URL in annotations") + return i.store.GetImageByURL(ctx, uri) + } + + token := strings.TrimPrefix(uri, "token://") + i.logger.Debug("Received an image token in annotations", "token", token) + return i.store.GetImage(ctx, token) +} + +// getImageURI is a helper function to retrieve the image URI from the alert annotations as a string. +func getImageURI(alert *alertingNotify.Alert) (string, error) { + uri, ok := alert.Annotations[alertingModels.ImageTokenAnnotation] + if !ok { + return "", alertingImages.ErrNoImageForAlert + } + return string(uri), nil +} + +// openImage returns an the io representation of an image from the given path. +func openImage(path string) (io.ReadCloser, error) { + fp := filepath.Clean(path) + _, err := os.Stat(fp) + if os.IsNotExist(err) || os.IsPermission(err) { + return nil, alertingImages.ErrImageNotFound + } + + f, err := os.Open(fp) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/pkg/services/ngalert/notifier/images_test.go b/pkg/services/ngalert/notifier/images_test.go index ba67ba52cb2..b16bd0e6360 100644 --- a/pkg/services/ngalert/notifier/images_test.go +++ b/pkg/services/ngalert/notifier/images_test.go @@ -2,16 +2,25 @@ package notifier import ( "context" + "io" + "os" + "path/filepath" "testing" + "time" + alertingImages "github.com/grafana/alerting/images" + alertingModels "github.com/grafana/alerting/models" + alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/prometheus/common/model" "github.com/stretchr/testify/require" ) func TestGetImage(t *testing.T) { fakeImageStore := store.NewFakeImageStore(t) - store := newImageStore(fakeImageStore) + store := newImageProvider(fakeImageStore, log.NewNopLogger()) t.Run("queries by token when it gets a token", func(tt *testing.T) { img := models.Image{ @@ -22,6 +31,7 @@ func TestGetImage(t *testing.T) { err := fakeImageStore.SaveImage(context.Background(), &img) require.NoError(tt, err) + // nolint:staticcheck savedImg, err := store.GetImage(context.Background(), "token://"+img.Token) require.NoError(tt, err) require.Equal(tt, savedImg.Token, img.Token) @@ -38,6 +48,7 @@ func TestGetImage(t *testing.T) { err := fakeImageStore.SaveImage(context.Background(), &img) require.NoError(tt, err) + // nolint:staticcheck savedImg, err := store.GetImage(context.Background(), img.URL) require.NoError(tt, err) require.Equal(tt, savedImg.Token, img.Token) @@ -45,3 +56,170 @@ func TestGetImage(t *testing.T) { require.Equal(tt, savedImg.Path, img.Path) }) } + +func TestGetImageURL(t *testing.T) { + var ( + imageWithoutURL = models.Image{ + Token: "test-no-url", + CreatedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(24 * time.Hour), + } + testImage = models.Image{ + Token: "test", + URL: "https://test.com", + CreatedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(24 * time.Hour), + } + ) + + fakeImageStore := store.NewFakeImageStore(t, &imageWithoutURL, &testImage) + store := newImageProvider(fakeImageStore, log.NewNopLogger()) + + tests := []struct { + name string + uri string + expURL string + expErr error + }{ + { + "URL does not exist", + "https://invalid.com/test", + "", + alertingImages.ErrImageNotFound, + }, { + "existing URL", + testImage.URL, + testImage.URL, + nil, + }, { + "token does not exist", + "token://invalid", + "", + alertingImages.ErrImageNotFound, + }, { + "existing token", + "token://" + testImage.Token, + testImage.URL, + nil, + }, { + "image has no URL", + "token://" + imageWithoutURL.Token, + "", + alertingImages.ErrImagesNoURL, + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + alert := alertingNotify.Alert{ + Alert: model.Alert{ + Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.uri)}, + }, + } + url, err := store.GetImageURL(context.Background(), &alert) + require.ErrorIs(tt, err, test.expErr) + require.Equal(tt, test.expURL, url) + }) + } +} + +func TestGetRawImage(t *testing.T) { + var ( + testBytes = []byte("some test bytes") + testPath = generateTestFile(t, testBytes) + imageWithoutPath = models.Image{ + Token: "test-no-path", + URL: "https://test-no-path.com", + CreatedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(24 * time.Hour), + } + testImage = models.Image{ + Token: "test", + URL: "https://test.com", + Path: testPath, + CreatedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(24 * time.Hour), + } + ) + + fakeImageStore := store.NewFakeImageStore(t, &imageWithoutPath, &testImage) + store := newImageProvider(fakeImageStore, log.NewNopLogger()) + + tests := []struct { + name string + uri string + expFilename string + expBytes []byte + expErr error + }{ + { + "URL does not exist", + "https://invalid.com/test", + "", + nil, + alertingImages.ErrImageNotFound, + }, { + "existing URL", + testImage.URL, + filepath.Base(testPath), + testBytes, + nil, + }, { + "token does not exist", + "token://invalid", + "", + nil, + alertingImages.ErrImageNotFound, + }, { + "existing token", + "token://" + testImage.Token, + filepath.Base(testPath), + testBytes, + nil, + }, { + "image has no path", + "token://" + imageWithoutPath.Token, + "", + nil, + alertingImages.ErrImagesNoPath, + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + alert := alertingNotify.Alert{ + Alert: model.Alert{ + Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.uri)}, + }, + } + readCloser, filename, err := store.GetRawImage(context.Background(), &alert) + require.ErrorIs(tt, err, test.expErr) + require.Equal(tt, test.expFilename, filename) + + if test.expBytes != nil { + b, err := io.ReadAll(readCloser) + require.NoError(tt, err) + require.Equal(tt, test.expBytes, b) + require.NoError(t, readCloser.Close()) + } + }) + } +} + +func generateTestFile(t *testing.T, b []byte) string { + t.Helper() + f, err := os.CreateTemp("/tmp", "image") + require.NoError(t, err) + defer func(f *os.File) { + _ = f.Close() + }(f) + + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(f.Name())) + }) + + _, err = f.Write(b) + require.NoError(t, err) + + return f.Name() +} diff --git a/pkg/services/ngalert/notifier/testing.go b/pkg/services/ngalert/notifier/testing.go index 31606030bf6..cab6386561d 100644 --- a/pkg/services/ngalert/notifier/testing.go +++ b/pkg/services/ngalert/notifier/testing.go @@ -13,6 +13,8 @@ import ( "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + + alertingImages "github.com/grafana/alerting/images" ) type fakeConfigStore struct { @@ -24,19 +26,23 @@ type fakeConfigStore struct { // Saves the image or returns an error. func (f *fakeConfigStore) SaveImage(ctx context.Context, img *models.Image) error { - return models.ErrImageNotFound + return alertingImages.ErrImageNotFound } func (f *fakeConfigStore) GetImage(ctx context.Context, token string) (*models.Image, error) { - return nil, models.ErrImageNotFound + return nil, alertingImages.ErrImageNotFound } func (f *fakeConfigStore) GetImageByURL(ctx context.Context, url string) (*models.Image, error) { - return nil, models.ErrImageNotFound + return nil, alertingImages.ErrImageNotFound +} + +func (f *fakeConfigStore) URLExists(ctx context.Context, url string) (bool, error) { + return false, alertingImages.ErrImageNotFound } func (f *fakeConfigStore) GetImages(ctx context.Context, tokens []string) ([]models.Image, []string, error) { - return nil, nil, models.ErrImageNotFound + return nil, nil, alertingImages.ErrImageNotFound } func NewFakeConfigStore(t *testing.T, configs map[int64]*models.AlertConfiguration) *fakeConfigStore { diff --git a/pkg/services/ngalert/store/image.go b/pkg/services/ngalert/store/image.go index 095098138bb..3c35326d15b 100644 --- a/pkg/services/ngalert/store/image.go +++ b/pkg/services/ngalert/store/image.go @@ -31,6 +31,10 @@ type ImageStore interface { // SaveImage saves the image or returns an error. SaveImage(ctx context.Context, img *models.Image) error + + // URLExists takes a URL and returns a boolean indicating whether or not + // we have an image for that URL. + URLExists(ctx context.Context, url string) (bool, error) } type ImageAdminStore interface { @@ -75,6 +79,19 @@ func (st DBstore) GetImageByURL(ctx context.Context, url string) (*models.Image, return &image, nil } +func (st DBstore) URLExists(ctx context.Context, url string) (bool, error) { + var exists bool + err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + ok, err := sess.Table("alert_image").Where("url = ? AND expires_at > ?", url, TimeNow().UTC()).Exist() + if err != nil { + return err + } + exists = ok + return nil + }) + return exists, err +} + func (st DBstore) GetImages(ctx context.Context, tokens []string) ([]models.Image, []string, error) { var images []models.Image if err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index 3cd4ba52e75..bb30ff2ef1f 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -9,10 +9,15 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func NewFakeImageStore(t *testing.T) *FakeImageStore { +func NewFakeImageStore(t *testing.T, images ...*models.Image) *FakeImageStore { + imageMap := make(map[string]*models.Image) + for _, image := range images { + imageMap[image.Token] = image + } + return &FakeImageStore{ t: t, - images: make(map[string]*models.Image), + images: imageMap, } } @@ -43,6 +48,17 @@ func (s *FakeImageStore) GetImageByURL(_ context.Context, url string) (*models.I return nil, models.ErrImageNotFound } +func (s *FakeImageStore) URLExists(_ context.Context, url string) (bool, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + for _, image := range s.images { + if image.URL == url { + return true, nil + } + } + return false, nil +} + func (s *FakeImageStore) GetImages(_ context.Context, tokens []string) ([]models.Image, []string, error) { s.mtx.Lock() defer s.mtx.Unlock() diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index 5f4001e74d2..78add4d1ce5 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -24,6 +24,7 @@ func AddTablesMigrations(mg *migrator.Migrator) { mg.AddMigration("add last_applied column to alert_configuration_history", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_configuration_history"}, &migrator.Column{ Name: "last_applied", Type: migrator.DB_Int, Nullable: false, Default: "0", })) + // End of migration log, add new migrations above this line. } // historicalTableMigrations contains those migrations that existed prior to creating the improved messaging around migration immutability. diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 2990e0d21ee..bfec5a4cd63 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -2312,9 +2312,9 @@ var expEmailNotifications = []*notifications.SendEmailCommandSync{ Annotations: template.KV{}, StartsAt: time.Time{}, EndsAt: time.Time{}, - GeneratorURL: "http://localhost:3000/alerting/grafana/UID_EmailAlert/view", + GeneratorURL: "http://localhost:3000/alerting/grafana/UID_EmailAlert/view?orgId=1", Fingerprint: "1e8f5e886dc14813", - SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DEmailAlert&matcher=grafana_folder%3Ddefault", + SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DEmailAlert&matcher=grafana_folder%3Ddefault&orgId=1", DashboardURL: "", PanelURL: "", Values: map[string]float64{"A": 1}, @@ -2366,7 +2366,7 @@ var expNonEmailNotifications = map[string][]string{ { "title": "[FIRING:1] SlackAlert2 (default)", "title_link": "http://localhost:3000/alerting/list", - "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = SlackAlert2\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_SlackAlert2/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSlackAlert2&matcher=grafana_folder%%3Ddefault\n", + "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = SlackAlert2\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_SlackAlert2/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSlackAlert2&matcher=grafana_folder%%3Ddefault&orgId=1\n", "fallback": "[FIRING:1] SlackAlert2 (default)", "footer": "Grafana v", "footer_icon": "https://grafana.com/static/assets/img/fav32.png", @@ -2391,7 +2391,7 @@ var expNonEmailNotifications = map[string][]string{ "component": "Integration Test", "group": "testgroup", "custom_details": { - "firing": "\nValue: A=1\nLabels:\n - alertname = PagerdutyAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_PagerdutyAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DPagerdutyAlert&matcher=grafana_folder%%3Ddefault\n", + "firing": "\nValue: A=1\nLabels:\n - alertname = PagerdutyAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_PagerdutyAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DPagerdutyAlert&matcher=grafana_folder%%3Ddefault&orgId=1\n", "num_firing": "1", "num_resolved": "0", "resolved": "" @@ -2411,7 +2411,7 @@ var expNonEmailNotifications = map[string][]string{ `{ "link": { "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2F%2Flocalhost%3A3000%2Falerting%2Flist", - "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = DingDingAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_DingDingAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDingDingAlert&matcher=grafana_folder%3Ddefault\n", + "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = DingDingAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_DingDingAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDingDingAlert&matcher=grafana_folder%3Ddefault&orgId=1\n", "title": "[FIRING:1] DingDingAlert (default)" }, "msgtype": "link" @@ -2432,7 +2432,7 @@ var expNonEmailNotifications = map[string][]string{ "weight": "bolder", "wrap": true }, { - "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = TeamsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TeamsAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DTeamsAlert\u0026matcher=grafana_folder%3Ddefault\n", + "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = TeamsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TeamsAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DTeamsAlert\u0026matcher=grafana_folder%3Ddefault&orgId=1\n", "type": "TextBlock", "wrap": true }, { @@ -2476,9 +2476,9 @@ var expNonEmailNotifications = map[string][]string{ "values": {"A": 1}, "valueString": "[ var='A' labels={} value=1 ]", "endsAt": "0001-01-01T00:00:00Z", - "generatorURL": "http://localhost:3000/alerting/grafana/UID_WebhookAlert/view", + "generatorURL": "http://localhost:3000/alerting/grafana/UID_WebhookAlert/view?orgId=1", "fingerprint": "15c59b0a380bd9f1", - "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert&matcher=grafana_folder%%3Ddefault", + "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert&matcher=grafana_folder%%3Ddefault&orgId=1", "dashboardURL": "", "panelURL": "" } @@ -2497,12 +2497,12 @@ var expNonEmailNotifications = map[string][]string{ "truncatedAlerts": 0, "title": "[FIRING:1] WebhookAlert (default)", "state": "alerting", - "message": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = WebhookAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_WebhookAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert&matcher=grafana_folder%%3Ddefault\n" + "message": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = WebhookAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_WebhookAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DWebhookAlert&matcher=grafana_folder%%3Ddefault&orgId=1\n" }`, }, "discord_recv/discord_test": { `{ - "content": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = DiscordAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_DiscordAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDiscordAlert&matcher=grafana_folder%3Ddefault\n", + "content": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = DiscordAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_DiscordAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DDiscordAlert&matcher=grafana_folder%3Ddefault&orgId=1\n", "embeds": [ { "color": 14037554, @@ -2530,7 +2530,7 @@ var expNonEmailNotifications = map[string][]string{ }, "name": "default" }, - "output": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = SensuGoAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_SensuGoAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSensuGoAlert&matcher=grafana_folder%%3Ddefault\n", + "output": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = SensuGoAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_SensuGoAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DSensuGoAlert&matcher=grafana_folder%%3Ddefault&orgId=1\n", "status": 2 }, "entity": { @@ -2543,10 +2543,10 @@ var expNonEmailNotifications = map[string][]string{ }`, }, "pushover_recv/pushover_test": { - "--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert (default)\r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: A=1\nLabels:\n - alertname = PushoverAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_PushoverAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DPushoverAlert&matcher=grafana_folder%3Ddefault\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n", + "--abcd\r\nContent-Disposition: form-data; name=\"user\"\r\n\r\nmysecretkey\r\n--abcd\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nmysecrettoken\r\n--abcd\r\nContent-Disposition: form-data; name=\"priority\"\r\n\r\n0\r\n--abcd\r\nContent-Disposition: form-data; name=\"sound\"\r\n\r\n\r\n--abcd\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n[FIRING:1] PushoverAlert (default)\r\n--abcd\r\nContent-Disposition: form-data; name=\"url\"\r\n\r\nhttp://localhost:3000/alerting/list\r\n--abcd\r\nContent-Disposition: form-data; name=\"url_title\"\r\n\r\nShow alert rule\r\n--abcd\r\nContent-Disposition: form-data; name=\"message\"\r\n\r\n**Firing**\n\nValue: A=1\nLabels:\n - alertname = PushoverAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_PushoverAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DPushoverAlert&matcher=grafana_folder%3Ddefault&orgId=1\r\n--abcd\r\nContent-Disposition: form-data; name=\"html\"\r\n\r\n1\r\n--abcd--\r\n", }, "telegram_recv/bot6sh027hs034h": { - "--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nHTML\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: A=1\nLabels:\n - alertname = TelegramAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TelegramAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTelegramAlert&matcher=grafana_folder%3Ddefault\n\r\n--abcd--\r\n", + "--abcd\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\ntelegram_chat_id\r\n--abcd\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nHTML\r\n--abcd\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\n**Firing**\n\nValue: A=1\nLabels:\n - alertname = TelegramAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TelegramAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTelegramAlert&matcher=grafana_folder%3Ddefault&orgId=1\n\r\n--abcd--\r\n", }, "googlechat_recv/googlechat_test": { `{ @@ -2562,7 +2562,7 @@ var expNonEmailNotifications = map[string][]string{ "widgets": [ { "textParagraph": { - "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = GoogleChatAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_GoogleChatAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DGoogleChatAlert&matcher=grafana_folder%%3Ddefault\n" + "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = GoogleChatAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_GoogleChatAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DGoogleChatAlert&matcher=grafana_folder%%3Ddefault&orgId=1\n" } }, { @@ -2600,7 +2600,7 @@ var expNonEmailNotifications = map[string][]string{ "client": "Grafana", "client_url": "http://localhost:3000/alerting/list", "description": "[FIRING:1] KafkaAlert (default)", - "details": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = KafkaAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_KafkaAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DKafkaAlert&matcher=grafana_folder%3Ddefault\n", + "details": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = KafkaAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_KafkaAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DKafkaAlert&matcher=grafana_folder%3Ddefault&orgId=1\n", "incident_key": "35c0bdb1715f9162a20d7b2a01cb2e3a4c5b1dc663571701e3f67212b696332f" } } @@ -2608,10 +2608,10 @@ var expNonEmailNotifications = map[string][]string{ }`, }, "line_recv/line_test": { - `message=%5BFIRING%3A1%5D+LineAlert+%28default%29%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+A%3D1%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0A+-+grafana_folder+%3D+default%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fgrafana%2FUID_LineAlert%2Fview%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DLineAlert%26matcher%3Dgrafana_folder%253Ddefault%0A`, + `message=%5BFIRING%3A1%5D+LineAlert+%28default%29%0Ahttp%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+A%3D1%0ALabels%3A%0A+-+alertname+%3D+LineAlert%0A+-+grafana_folder+%3D+default%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fgrafana%2FUID_LineAlert%2Fview%3ForgId%3D1%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DLineAlert%26matcher%3Dgrafana_folder%253Ddefault%26orgId%3D1%0A`, }, "threema_recv/threema_test": { - `from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%28default%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+A%3D1%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0A+-+grafana_folder+%3D+default%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fgrafana%2FUID_ThreemaAlert%2Fview%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DThreemaAlert%26matcher%3Dgrafana_folder%253Ddefault%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`, + `from=%2A1234567&secret=myapisecret&text=%E2%9A%A0%EF%B8%8F+%5BFIRING%3A1%5D+ThreemaAlert+%28default%29%0A%0A%2AMessage%3A%2A%0A%2A%2AFiring%2A%2A%0A%0AValue%3A+A%3D1%0ALabels%3A%0A+-+alertname+%3D+ThreemaAlert%0A+-+grafana_folder+%3D+default%0AAnnotations%3A%0ASource%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fgrafana%2FUID_ThreemaAlert%2Fview%3ForgId%3D1%0ASilence%3A+http%3A%2F%2Flocalhost%3A3000%2Falerting%2Fsilence%2Fnew%3Falertmanager%3Dgrafana%26matcher%3Dalertname%253DThreemaAlert%26matcher%3Dgrafana_folder%253Ddefault%26orgId%3D1%0A%0A%2AURL%3A%2A+http%3A%2Flocalhost%3A3000%2Falerting%2Flist%0A&to=abcdefgh`, }, "victorops_recv/victorops_test": { `{ @@ -2620,14 +2620,14 @@ var expNonEmailNotifications = map[string][]string{ "entity_id": "633ae988fa7074bcb51f3d1c5fef2ba1c5c4ccb45b3ecbf681f7d507b078b1ae", "message_type": "CRITICAL", "monitoring_tool": "Grafana v", - "state_message": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = VictorOpsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_VictorOpsAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DVictorOpsAlert&matcher=grafana_folder%%3Ddefault\n", + "state_message": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = VictorOpsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_VictorOpsAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%%3DVictorOpsAlert&matcher=grafana_folder%%3Ddefault&orgId=1\n", "timestamp": %s }`, }, "opsgenie_recv/opsgenie_test": { `{ "alias": "47e92f0f6ef9fe99f3954e0d6155f8d09c4b9a038d8c3105e82c0cee4c62956e", - "description": "[FIRING:1] OpsGenieAlert (default)\nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: A=1\nLabels:\n - alertname = OpsGenieAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_OpsGenieAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DOpsGenieAlert&matcher=grafana_folder%3Ddefault\n", + "description": "[FIRING:1] OpsGenieAlert (default)\nhttp://localhost:3000/alerting/list\n\n**Firing**\n\nValue: A=1\nLabels:\n - alertname = OpsGenieAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_OpsGenieAlert/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DOpsGenieAlert&matcher=grafana_folder%3Ddefault&orgId=1\n", "details": { "url": "http://localhost:3000/alerting/list" },