Alerting: Use new image TokenProvider and send image url in annotation (#99989)
* Send new annotation containing image url * Use new image TokenProvider with TokenStore New abstraction GetImage no longer needs to support parsing both token and url from annotations, as remote AM will use the new URLProvider. Instead, we use the new generic TokenProvider and give it a TokenStore backed by the grafana database. That means we revert back to always using token simplifying code and security considerations. * Upgrade grafana/alerting to merged commit SHA
This commit is contained in:
@@ -71,7 +71,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
|
||||
@@ -1511,8 +1511,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY=
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts=
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
|
||||
@@ -6,8 +6,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrImageNotFound is returned when the image does not exist.
|
||||
// ErrImageNotFound is returned when the image does not exist or is expired.
|
||||
ErrImageNotFound = errors.New("image not found")
|
||||
|
||||
// ErrImageDataUnavailable is returned when image data is unavailable. Usually because the image is missing a path.
|
||||
ErrImageDataUnavailable = errors.New("image data is unavailable")
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
|
||||
@@ -6,145 +6,61 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
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 imageProvider struct {
|
||||
type tokenStore struct {
|
||||
store store.ImageStore
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
var _ alertingImages.TokenStore = (*tokenStore)(nil)
|
||||
|
||||
func newImageProvider(store store.ImageStore, logger log.Logger) alertingImages.Provider {
|
||||
return &imageProvider{
|
||||
return alertingImages.NewTokenProvider(&tokenStore{
|
||||
store: store,
|
||||
logger: logger,
|
||||
}
|
||||
}, newLogWrapper(logger))
|
||||
}
|
||||
|
||||
func (i imageProvider) GetImage(ctx context.Context, uri string) (*alertingImages.Image, error) {
|
||||
image, err := i.getImageFromURI(ctx, uri)
|
||||
func (t tokenStore) GetImage(ctx context.Context, token string) (*alertingImages.Image, error) {
|
||||
image, err := t.store.GetImage(ctx, token)
|
||||
if err != nil {
|
||||
if errors.Is(err, models.ErrImageNotFound) {
|
||||
i.logger.Info("Image not found in database", "uri", uri)
|
||||
return nil, alertingImages.ErrImageNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &alertingImages.Image{
|
||||
Token: image.Token,
|
||||
Path: image.Path,
|
||||
URL: image.URL,
|
||||
CreatedAt: image.CreatedAt,
|
||||
URL: image.URL,
|
||||
RawData: func(_ context.Context) (alertingImages.ImageContent, error) {
|
||||
if image.Path == "" {
|
||||
return alertingImages.ImageContent{}, models.ErrImageDataUnavailable
|
||||
}
|
||||
b, err := readImage(image.Path, t.logger)
|
||||
if err != nil {
|
||||
return alertingImages.ImageContent{}, err
|
||||
}
|
||||
return alertingImages.ImageContent{
|
||||
Name: filepath.Base(image.Path),
|
||||
Content: b,
|
||||
}, nil
|
||||
},
|
||||
}, 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) {
|
||||
// readImage returns an image from the given path.
|
||||
func readImage(path string, logger log.Logger) ([]byte, error) {
|
||||
fp := filepath.Clean(path)
|
||||
_, err := os.Stat(fp)
|
||||
if os.IsNotExist(err) || os.IsPermission(err) {
|
||||
return nil, alertingImages.ErrImageNotFound
|
||||
return nil, models.ErrImageNotFound
|
||||
}
|
||||
|
||||
f, err := os.Open(fp)
|
||||
@@ -152,5 +68,11 @@ func openImage(path string) (io.ReadCloser, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logger.Error("Failed to close image file", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -11,177 +10,98 @@ import (
|
||||
alertingImages "github.com/grafana/alerting/images"
|
||||
alertingModels "github.com/grafana/alerting/models"
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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 := newImageProvider(fakeImageStore, log.NewNopLogger())
|
||||
testBytes := []byte("some test bytes")
|
||||
testPath := generateTestFile(t, testBytes)
|
||||
|
||||
t.Run("queries by token when it gets a token", func(tt *testing.T) {
|
||||
img := models.Image{
|
||||
Token: "test",
|
||||
URL: "http://localhost:1234",
|
||||
Path: "test.png",
|
||||
}
|
||||
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)
|
||||
require.Equal(tt, savedImg.URL, img.URL)
|
||||
require.Equal(tt, savedImg.Path, img.Path)
|
||||
})
|
||||
|
||||
t.Run("queries by URL when it gets a URL", func(tt *testing.T) {
|
||||
img := models.Image{
|
||||
Token: "test",
|
||||
Path: "test.png",
|
||||
URL: "https://test.com/test.png",
|
||||
}
|
||||
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)
|
||||
require.Equal(tt, savedImg.URL, img.URL)
|
||||
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",
|
||||
imageWithoutPath = models.Image{
|
||||
Token: "test-token-no-path",
|
||||
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",
|
||||
Token: "test-token",
|
||||
URL: "https://test.com",
|
||||
Path: testPath,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour),
|
||||
}
|
||||
testImageMissingFile = models.Image{
|
||||
Token: "test-token-missing-file",
|
||||
URL: "https://test.com",
|
||||
Path: "/tmp/missing/1234asdf.png",
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour),
|
||||
}
|
||||
)
|
||||
|
||||
fakeImageStore := store.NewFakeImageStore(t, &imageWithoutPath, &testImage)
|
||||
fakeImageStore := store.NewFakeImageStore(t, &imageWithoutPath, &testImage, &testImageMissingFile)
|
||||
store := newImageProvider(fakeImageStore, log.NewNopLogger())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
expFilename string
|
||||
expBytes []byte
|
||||
expErr error
|
||||
name string
|
||||
token string
|
||||
url string
|
||||
expImage *alertingImages.Image
|
||||
expImageContent *alertingImages.ImageContent
|
||||
expRawDataErr error
|
||||
}{
|
||||
{
|
||||
"URL does not exist",
|
||||
"https://invalid.com/test",
|
||||
"",
|
||||
nil,
|
||||
alertingImages.ErrImageNotFound,
|
||||
name: "Given existing raw token, expect image",
|
||||
token: testImage.Token,
|
||||
expImage: &alertingImages.Image{
|
||||
URL: testImage.URL,
|
||||
},
|
||||
}, {
|
||||
"existing URL",
|
||||
testImage.URL,
|
||||
filepath.Base(testPath),
|
||||
testBytes,
|
||||
nil,
|
||||
name: "Given existing token and url, expect image",
|
||||
token: testImage.Token,
|
||||
url: testImage.URL,
|
||||
expImage: &alertingImages.Image{
|
||||
URL: testImage.URL,
|
||||
},
|
||||
}, {
|
||||
"token does not exist",
|
||||
"token://invalid",
|
||||
"",
|
||||
nil,
|
||||
alertingImages.ErrImageNotFound,
|
||||
name: "Given existing with just url, expect nil",
|
||||
token: "",
|
||||
url: testImage.URL,
|
||||
expImage: nil,
|
||||
}, {
|
||||
"existing token",
|
||||
"token://" + testImage.Token,
|
||||
filepath.Base(testPath),
|
||||
testBytes,
|
||||
nil,
|
||||
name: "Given missing raw token, expect nil",
|
||||
token: "invalid",
|
||||
expImage: nil,
|
||||
}, {
|
||||
"image has no path",
|
||||
"token://" + imageWithoutPath.Token,
|
||||
"",
|
||||
nil,
|
||||
alertingImages.ErrImagesNoPath,
|
||||
name: "Given image with Path, expect RawData",
|
||||
token: testImage.Token,
|
||||
expImage: &alertingImages.Image{
|
||||
URL: testImage.URL,
|
||||
},
|
||||
expImageContent: &alertingImages.ImageContent{
|
||||
Name: filepath.Base(testImage.Path),
|
||||
Content: testBytes,
|
||||
},
|
||||
}, {
|
||||
name: "Given image with Path but file doesn't exist, expect RawData error",
|
||||
token: testImageMissingFile.Token,
|
||||
expImage: &alertingImages.Image{
|
||||
URL: testImageMissingFile.URL,
|
||||
},
|
||||
expRawDataErr: models.ErrImageNotFound,
|
||||
}, {
|
||||
name: "Given image without Path, expect RawData error",
|
||||
token: imageWithoutPath.Token,
|
||||
expImage: &alertingImages.Image{
|
||||
URL: imageWithoutPath.URL,
|
||||
},
|
||||
expRawDataErr: models.ErrImageDataUnavailable,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -189,18 +109,24 @@ func TestGetRawImage(t *testing.T) {
|
||||
t.Run(test.name, func(tt *testing.T) {
|
||||
alert := alertingNotify.Alert{
|
||||
Alert: model.Alert{
|
||||
Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.uri)},
|
||||
Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.token)},
|
||||
},
|
||||
}
|
||||
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)
|
||||
image, err := store.GetImage(context.Background(), alert)
|
||||
require.NoError(tt, err)
|
||||
if test.expImage == nil {
|
||||
require.Nil(tt, image)
|
||||
return
|
||||
}
|
||||
require.Equal(tt, test.expImage.URL, image.URL)
|
||||
if test.expImageContent != nil {
|
||||
ic, err := image.RawData(context.Background())
|
||||
require.NoError(tt, err)
|
||||
require.Equal(tt, test.expBytes, b)
|
||||
require.NoError(t, readCloser.Close())
|
||||
require.Equal(tt, *test.expImageContent, ic)
|
||||
}
|
||||
if test.expRawDataErr != nil {
|
||||
_, err := image.RawData(context.Background())
|
||||
require.ErrorIs(tt, err, test.expRawDataErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ var LoggerFactory alertingLogging.LoggerFactory = func(logger string, ctx ...any
|
||||
return &logWrapper{log.New(append([]any{logger}, ctx...)...)}
|
||||
}
|
||||
|
||||
func newLogWrapper(logger log.Logger, ctx ...any) alertingLogging.Logger {
|
||||
return &logWrapper{logger.New(ctx...)}
|
||||
}
|
||||
|
||||
type logWrapper struct {
|
||||
*log.ConcreteLogger
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,8 +49,8 @@ func StateToPostableAlert(transition StateTransition, appURL *url.URL) *models.P
|
||||
nA[alertingModels.ValueStringAnnotation] = alertState.LastEvaluationString
|
||||
}
|
||||
|
||||
if alertState.Image != nil && alertState.Image.Token != "" {
|
||||
nA[alertingModels.ImageTokenAnnotation] = alertState.Image.Token
|
||||
if alertState.Image != nil {
|
||||
attachImageAnnotations(alertState.Image, nA)
|
||||
}
|
||||
|
||||
if alertState.StateReason != "" {
|
||||
@@ -153,3 +154,13 @@ func FromAlertsStateToStoppedAlert(firingStates []StateTransition, appURL *url.U
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
// attachImageAnnotations attaches image annotations to the alert.
|
||||
func attachImageAnnotations(image *ngModels.Image, a data.Labels) {
|
||||
if image.Token != "" {
|
||||
a[alertingModels.ImageTokenAnnotation] = image.Token
|
||||
}
|
||||
if image.URL != "" {
|
||||
a[alertingModels.ImageURLAnnotation] = image.URL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +120,10 @@ func Test_StateToPostableAlert(t *testing.T) {
|
||||
require.Equal(t, expected, result.Annotations)
|
||||
})
|
||||
|
||||
t.Run("add __alertImageToken__ if there is an image token", func(t *testing.T) {
|
||||
t.Run("add both annotations if there is an image token and url", func(t *testing.T) {
|
||||
alertState := randomTransition(eval.Normal, tc.state)
|
||||
alertState.Annotations = randomMapOfStrings()
|
||||
alertState.Image = &ngModels.Image{Token: "test_token"}
|
||||
alertState.Image = &ngModels.Image{Token: "test_token", URL: "test_url"}
|
||||
|
||||
result := StateToPostableAlert(alertState, appURL)
|
||||
|
||||
@@ -131,12 +131,17 @@ func Test_StateToPostableAlert(t *testing.T) {
|
||||
for k, v := range alertState.Annotations {
|
||||
expected[k] = v
|
||||
}
|
||||
expected["__alertImageToken__"] = alertState.Image.Token
|
||||
expected[alertingModels.ImageTokenAnnotation] = alertState.Image.Token
|
||||
expected[alertingModels.ImageURLAnnotation] = alertState.Image.URL
|
||||
|
||||
// Sanity check that the annotation is correct.
|
||||
require.Contains(t, result.Annotations[alertingModels.ImageTokenAnnotation], alertState.Image.Token)
|
||||
require.Contains(t, result.Annotations[alertingModels.ImageURLAnnotation], alertState.Image.URL)
|
||||
|
||||
require.Equal(t, expected, result.Annotations)
|
||||
})
|
||||
|
||||
t.Run("don't add __alertImageToken__ if there's no image token", func(t *testing.T) {
|
||||
t.Run("don't add annotations if there's no image token or url", func(t *testing.T) {
|
||||
alertState := randomTransition(eval.Normal, tc.state)
|
||||
alertState.Annotations = randomMapOfStrings()
|
||||
alertState.Image = &ngModels.Image{}
|
||||
|
||||
@@ -192,7 +192,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
|
||||
|
||||
@@ -566,8 +566,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY=
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts=
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
|
||||
@@ -117,7 +117,7 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
|
||||
|
||||
@@ -397,8 +397,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY=
|
||||
github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts=
|
||||
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
|
||||
Reference in New Issue
Block a user