Alerting: Remove the POST endpoint for the internal Grafana Alertmanager config (#103819)

* Remove POST config for Grafana Alertmanager

* Delete auth + test for removed path

* Alerting: Remove check for `alertingApiServer` toggle in UI (#103805)

* Remove check for alertingApiServer in UI

* Update tests to no longer care about alertingApiServer

* Add contact points handlers now that we use alertingApiServer all the time

* Fix test broken from removing camelCase for UIDs

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
pull/103949/head
William Wernert 1 month ago committed by GitHub
parent c47ab101d1
commit a5288db624
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pkg/registry/apis/alerting/notifications/register.go
  2. 2
      pkg/registry/apis/alerting/notifications/routingtree/conversions.go
  3. 4
      pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go
  4. 4
      pkg/registry/apis/alerting/notifications/timeinterval/conversions.go
  5. 8
      pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go
  6. 18
      pkg/services/navtree/navtreeimpl/navtree.go
  7. 7
      pkg/services/ngalert/accesscontrol.go
  8. 46
      pkg/services/ngalert/api/api_alertmanager.go
  9. 40
      pkg/services/ngalert/api/api_alertmanager_guards.go
  10. 172
      pkg/services/ngalert/api/api_alertmanager_test.go
  11. 13
      pkg/services/ngalert/api/authorization.go
  12. 7
      pkg/services/ngalert/api/forking_alertmanager.go
  13. 21
      pkg/services/ngalert/api/generated_base_api_alertmanager.go
  14. 11
      pkg/services/ngalert/api/tooling/definitions/alertmanager.go
  15. 32
      pkg/services/ngalert/api/tooling/post.json
  16. 32
      pkg/services/ngalert/api/tooling/spec.json
  17. 6
      pkg/services/store/testdata/public_testdata.golden.jsonc
  18. 48
      pkg/tests/api/alerting/api_admin_configuration_test.go
  19. 554
      pkg/tests/api/alerting/api_alertmanager_configuration_test.go
  20. 61
      pkg/tests/api/alerting/api_alertmanager_test.go
  21. 116
      pkg/tests/api/alerting/api_notification_channel_test.go
  22. 28
      pkg/tests/api/alerting/api_ruler_test.go
  23. 35
      pkg/tests/api/alerting/testing.go
  24. 79
      pkg/tests/apis/alerting/notifications/common/testing.go
  25. 165
      pkg/tests/apis/alerting/notifications/receivers/receiver_test.go
  26. 134
      pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go
  27. 33
      pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go
  28. 61
      pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go
  29. 101
      public/app/features/alerting/unified/MuteTimings.test.tsx
  30. 25
      public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx
  31. 25
      public/app/features/alerting/unified/Templates.test.tsx
  32. 25
      public/app/features/alerting/unified/__snapshots__/NotificationPoliciesPage.test.tsx.snap
  33. 1
      public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx
  34. 43
      public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx
  35. 55
      public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx
  36. 110
      public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap
  37. 36
      public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx
  38. 79
      public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx
  39. 36
      public/app/features/alerting/unified/components/mute-timings/mocks.ts
  40. 22
      public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx
  41. 110
      public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
  42. 74
      public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx
  43. 2
      public/app/features/alerting/unified/components/receivers/form/__snapshots__/GrafanaReceiverForm.test.tsx.snap
  44. 3
      public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx
  45. 14
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
  46. 9
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx
  47. 24
      public/app/features/alerting/unified/mocks/server/configure.ts
  48. 2
      public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts
  49. 70
      public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts
  50. 4
      public/app/features/alerting/unified/utils/k8s/utils.ts

@ -44,7 +44,7 @@ func RegisterAPIService(
cfg *setting.Cfg,
ng *ngalert.AlertNG,
) *NotificationsAPIBuilder {
if ng.IsDisabled() || !features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
if ng.IsDisabled() {
return nil
}
builder := &NotificationsAPIBuilder{

@ -19,7 +19,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func convertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) {
func ConvertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) {
spec := model.Spec{
Defaults: model.RouteDefaults{
GroupBy: r.GroupByStr,

@ -65,7 +65,7 @@ func (s *legacyStorage) getUserDefinedRoutingTree(ctx context.Context) (*model.R
if err != nil {
return nil, err
}
return convertToK8sResource(orgId, res, version, s.namespacer)
return ConvertToK8sResource(orgId, res, version, s.namespacer)
}
func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
@ -131,7 +131,7 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
return nil, false, err
}
obj, err = convertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer)
obj, err = ConvertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer)
return obj, false, err
}

@ -15,7 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
)
func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TimeIntervalList, error) {
func ConvertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TimeIntervalList, error) {
data, err := json.Marshal(intervals)
if err != nil {
return nil, err
@ -39,7 +39,7 @@ func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval
return result, nil
}
func convertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeInterval, error) {
func ConvertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeInterval, error) {
data, err := json.Marshal(interval)
if err != nil {
return nil, err

@ -67,7 +67,7 @@ func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOpti
return nil, err
}
return convertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector)
return ConvertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector)
}
func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) {
@ -83,7 +83,7 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
for _, mt := range timings {
if mt.UID == uid {
return convertToK8sResource(info.OrgID, mt, s.namespacer)
return ConvertToK8sResource(info.OrgID, mt, s.namespacer)
}
}
return nil, errors.NewNotFound(ResourceInfo.GroupResource(), uid)
@ -118,7 +118,7 @@ func (s *legacyStorage) Create(ctx context.Context,
if err != nil {
return nil, err
}
return convertToK8sResource(info.OrgID, out, s.namespacer)
return ConvertToK8sResource(info.OrgID, out, s.namespacer)
}
func (s *legacyStorage) Update(ctx context.Context,
@ -165,7 +165,7 @@ func (s *legacyStorage) Update(ctx context.Context,
return nil, false, err
}
r, err := convertToK8sResource(info.OrgID, updated, s.namespacer)
r, err := ConvertToK8sResource(info.OrgID, updated, s.namespacer)
return r, false, err
}

@ -429,20 +429,14 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
contactPointsPerms := []ac.Evaluator{
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
}
// With the new alerting API, we have other permissions to consider. We don't want to consider these with the old
// alerting API to maintain backwards compatibility.
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) {
contactPointsPerms = append(contactPointsPerms,
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
)
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
}
if hasAccess(ac.EvalAny(contactPointsPerms...)) {

@ -381,10 +381,9 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu
instancesReaderRole, instancesWriterRole,
notificationsReaderRole, notificationsWriterRole,
alertingReaderRole, alertingWriterRole, alertingAdminRole, alertingProvisionerRole, alertingProvisioningReaderWithSecretsRole, alertingProvisioningStatus,
}
if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole)
// k8s roles
receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole,
timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole,
}
return service.DeclareFixedRoles(fixedRoles...)

@ -188,52 +188,6 @@ func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *cont
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration activated"})
}
func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, body apimodels.PostableUserConfig) response.Response {
// Remove autogenerated config from the user config before checking provenance guard and eventually saving it.
// TODO: This and provenance guard should be moved to the notifier package.
notifier.RemoveAutogenConfigIfExists(body.AlertmanagerConfig.Route)
currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), false)
// If a config is present and valid we proceed with the guard, otherwise we
// just bypass the guard which is okay as we are anyway in an invalid state.
if err == nil {
if err := srv.provenanceGuard(currentConfig, body); err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
}
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) {
if err != nil {
// Unclear if returning an error here is the right thing to do, preventing the user from posting a new config
// when the current one is legitimately invalid is not optimal, but we need to ensure receiver
// permissions are maintained and prevent potential access control bypasses. The workaround is to use the
// various new k8s API endpoints to fix the configuration.
return ErrResp(http.StatusInternalServerError, err, "")
}
if err := srv.k8sApiServiceGuard(currentConfig, body); err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
}
err = srv.mam.SaveAndApplyAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), body)
if err == nil {
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"})
}
var unknownReceiverError notifier.UnknownReceiverError
if errors.As(err, &unknownReceiverError) {
return ErrResp(http.StatusBadRequest, unknownReceiverError, "")
}
var configRejectedError notifier.AlertmanagerConfigRejectedError
if errors.As(err, &configRejectedError) {
return ErrResp(http.StatusBadRequest, configRejectedError, "")
}
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) {
return response.Error(http.StatusNotFound, err.Error(), err)
}
if errors.Is(err, notifier.ErrAlertmanagerNotReady) {
return response.Error(http.StatusConflict, err.Error(), err)
}
return response.ErrOrFallback(http.StatusInternalServerError, err.Error(), err)
}
func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response {
am, errResp := srv.AlertmanagerFor(c.GetOrgID())
if errResp != nil {

@ -16,46 +16,6 @@ import (
"github.com/grafana/grafana/pkg/util/cmputil"
)
func (srv AlertmanagerSrv) provenanceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
if err := checkRoutes(currentConfig, newConfig); err != nil {
return err
}
if err := checkTemplates(currentConfig, newConfig); err != nil {
return err
}
if err := checkContactPoints(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers); err != nil {
return err
}
if err := checkMuteTimes(currentConfig, newConfig); err != nil {
return err
}
return nil
}
func (srv AlertmanagerSrv) k8sApiServiceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
// Modifications to receivers via this API is tricky with new per-receiver RBAC. Assuming we restrict the API to only
// those users with global edit permissions, we would still need to consider the following:
// - Since the UIDs stored in the database for the purposes of per-receiver RBAC are generated based on the receiver
// name, we would need to ensure continuity of permissions when a receiver is renamed. This would, preferably,
// require detecting renames and updating the permissions UID in the database.
// - Editors don't have permission to manage all receiver permissions by default, only admin users do. This means that
// certain combined operations (e.g. swapping the name of receivers) would require careful handling to ensure
// that the correct permissions are maintained, and considering receivers don't have a unique identifier outside
// their name, this is non-trivial.
// Neither of these are insurmountable, but considering this endpoint will be removed once FlagAlertingApiServer
// becomes GA, the complexity may not be worthwhile. To that end, for now we reject any request that attempts to
// update receivers, while allowing operations that add or remove receivers.
delta, err := calculateReceiversDelta(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers)
if err != nil {
return err
}
if len(delta.Updated) > 0 {
return fmt.Errorf("cannot update receivers using this API while per-receiver RBAC is enabled; either disable the `alertingApiServer` feature flag or use an API that supports per-receiver RBAC (e.g. provisioning or receivers API)")
}
return nil
}
func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
reporter := cmputil.DiffReporter{}
options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{}), cmp.Transformer("", func(regexp amConfig.Regexp) any {

@ -2,7 +2,6 @@ package api
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/json"
"net/http"
@ -101,90 +100,6 @@ func TestContextWithTimeoutFromRequest(t *testing.T) {
}
func TestAlertmanagerConfig(t *testing.T) {
sut := createSut(t)
t.Run("assert 404 Not Found when applying config to nonexistent org", func(t *testing.T) {
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 12,
},
}
request := createAmConfigRequest(t, validConfig)
response := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 404, response.Status())
require.Contains(t, string(response.Body()), "Alertmanager does not exist for this organization")
})
t.Run("assert 202 when config successfully applied", func(t *testing.T) {
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
},
}
request := createAmConfigRequest(t, validConfig)
response := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 202, response.Status())
})
t.Run("assert 202 when alertmanager to configure is not ready", func(t *testing.T) {
sut := createSut(t)
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 3, // Org 3 was initialized with broken config.
},
}
request := createAmConfigRequest(t, validConfig)
response := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 202, response.Status())
})
t.Run("assert config hash doesn't change when sending RouteGetAlertingConfig back to RoutePostAlertingConfig", func(t *testing.T) {
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
},
}
request := createAmConfigRequest(t, validConfigWithSecureSetting)
r := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 202, r.Status())
getResponse := sut.RouteGetAlertingConfig(&rc)
require.Equal(t, 200, getResponse.Status())
body := getResponse.Body()
hash := md5.Sum(body)
postable, err := notifier.Load(body)
require.NoError(t, err)
r = sut.RoutePostAlertingConfig(&rc, *postable)
require.Equal(t, 202, r.Status())
getResponse = sut.RouteGetAlertingConfig(&rc)
require.Equal(t, 200, getResponse.Status())
newHash := md5.Sum(getResponse.Body())
require.Equal(t, hash, newHash)
})
t.Run("when objects are not provisioned", func(t *testing.T) {
t.Run("route from GET config has no provenance", func(t *testing.T) {
sut := createSut(t)
@ -229,9 +144,8 @@ func TestAlertmanagerConfig(t *testing.T) {
t.Run("contact point from GET config has expected provenance", func(t *testing.T) {
sut := createSut(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfig)
_ = sut.RoutePostAlertingConfig(rc, request)
RoutePostAlertingConfig(t, sut.mam, rc, validConfig)
response := sut.RouteGetAlertingConfig(rc)
body := asGettableUserConfig(t, response)
@ -347,11 +261,8 @@ func TestGetAlertmanagerConfiguration_NewSecretField(t *testing.T) {
}
}
`
postable := createAmConfigRequest(t, postWithoutChanges)
res = sut.RoutePostAlertingConfig(rc, postable)
require.Equal(t, 202, res.Status())
RoutePostAlertingConfig(t, sut.mam, rc, postWithoutChanges)
// Check that the secret field "integrationKey" is now encrypted in SecureSettings.
savedConfig := &apimodels.PostableUserConfig{}
err = json.Unmarshal([]byte(configs[orgId].AlertmanagerConfiguration), savedConfig)
@ -400,31 +311,6 @@ func TestAlertmanagerAutogenConfig(t *testing.T) {
}
}
t.Run("route POST config", func(t *testing.T) {
t.Run("does not save autogen routes", func(t *testing.T) {
sut, configs := createSutForAutogen(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfigWithAutogen)
response := sut.RoutePostAlertingConfig(rc, request)
require.Equal(t, 202, response.Status())
compare(t, validConfigWithoutAutogen, configs[1].AlertmanagerConfiguration)
})
t.Run("provenance guard ignores autogen routes", func(t *testing.T) {
sut := createSut(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfigWithoutAutogen)
_ = sut.RoutePostAlertingConfig(rc, request)
setRouteProvenance(t, 1, sut.mam.ProvStore)
request = createAmConfigRequest(t, validConfigWithAutogen)
request.AlertmanagerConfig.Route.Provenance = apimodels.Provenance(ngmodels.ProvenanceAPI)
response := sut.RoutePostAlertingConfig(rc, request)
require.Equal(t, 202, response.Status())
})
})
t.Run("route GET config", func(t *testing.T) {
t.Run("when admin return autogen routes", func(t *testing.T) {
sut, _ := createSutForAutogen(t)
@ -691,16 +577,6 @@ func createSut(t *testing.T) AlertmanagerSrv {
}
}
func createAmConfigRequest(t *testing.T, config string) apimodels.PostableUserConfig {
t.Helper()
request := apimodels.PostableUserConfig{}
err := request.UnmarshalJSON([]byte(config))
require.NoError(t, err)
return request
}
func createMultiOrgAlertmanager(t *testing.T, configs map[int64]*ngmodels.AlertConfiguration) *notifier.MultiOrgAlertmanager {
t.Helper()
@ -849,40 +725,6 @@ var validConfigWithAutogen = `{
}
`
var validConfigWithSecureSetting = `{
"template_files": {
"a": "template"
},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email"
},
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "email receiver",
"type": "email",
"settings": {
"addresses": "<example@email.com>"
}
}]},
{
"name": "slack",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "slack1",
"type": "slack",
"settings": {"text": "slack text"},
"secureSettings": {
"url": "secure url"
}
}]
}]
}
}
`
var brokenConfig = `
"alertmanager_config": {
"route": {
@ -947,3 +789,13 @@ func asGettableHistoricUserConfigs(t *testing.T, r response.Response) []apimodel
require.NoError(t, err)
return body
}
// RoutePostAlertingConfig drop-in replacement for removed POST endpoint to make test transition easier.
func RoutePostAlertingConfig(t *testing.T, mam *notifier.MultiOrgAlertmanager, rc *contextmodel.ReqContext, amConfig string) {
t.Helper()
cfg := apimodels.PostableUserConfig{}
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = mam.SaveAndApplyAlertmanagerConfiguration(rc.Req.Context(), 1, cfg)
require.NoError(t, err)
}

@ -8,7 +8,6 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
@ -229,9 +228,6 @@ func (api *API) authorize(method, path string) web.Handler {
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodGet + "/api/alertmanager/grafana/api/v2/status":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/alerts":
// additional authorization is done in the request handler
eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite))
case http.MethodPost + "/api/alertmanager/grafana/config/history/{id}/_activate":
eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite))
case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/receivers":
@ -296,12 +292,9 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingProvisioningRead), // organization scope
ac.EvalPermission(ac.ActionAlertingNotificationsProvisioningRead), // organization scope
ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), // organization scope
}
if api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
perms = append(perms,
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
)
ac.EvalPermission(ac.ActionAlertingReceiversRead), // organization scope
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), // organization scope
}
eval = ac.EvalAny(perms...)

@ -175,13 +175,6 @@ func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *contextmodel
return f.GrafanaSvc.RouteGetSilences(ctx)
}
func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableUserConfig) response.Response {
if !conf.AlertmanagerConfig.ReceiverType().Can(apimodels.GrafanaReceiverType) {
return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.AlertmanagerConfig.ReceiverType().String()))
}
return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf)
}
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetReceivers(ctx)
}

@ -42,7 +42,6 @@ type AlertmanagerApi interface {
RouteGetSilences(*contextmodel.ReqContext) response.Response
RoutePostAMAlerts(*contextmodel.ReqContext) response.Response
RoutePostAlertingConfig(*contextmodel.ReqContext) response.Response
RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response
RoutePostGrafanaAlertingConfigHistoryActivate(*contextmodel.ReqContext) response.Response
RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response
RoutePostTestGrafanaTemplates(*contextmodel.ReqContext) response.Response
@ -162,14 +161,6 @@ func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *contextmodel.ReqCo
}
return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam)
}
func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.PostableUserConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.handleRoutePostGrafanaAlertingConfig(ctx, conf)
}
func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfigHistoryActivate(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
idParam := web.Params(ctx.Req)[":id"]
@ -458,18 +449,6 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics
m,
),
)
group.Post(
toMacaronPath("/api/alertmanager/grafana/config/api/v1/alerts"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/alertmanager/grafana/config/api/v1/alerts"),
metrics.Instrument(
http.MethodPost,
"/api/alertmanager/grafana/config/api/v1/alerts",
api.Hooks.Wrap(srv.RoutePostGrafanaAlertingConfig),
m,
),
)
group.Post(
toMacaronPath("/api/alertmanager/grafana/config/history/{id}/_activate"),
requestmeta.SetOwner(requestmeta.TeamAlerting),

@ -16,17 +16,6 @@ import (
alertingmodels "github.com/grafana/alerting/models"
)
// swagger:route POST /alertmanager/grafana/config/api/v1/alerts alertmanager RoutePostGrafanaAlertingConfig
//
// sets an Alerting config
//
// This API is designated to internal use only and can be removed or changed at any time without prior notice.
//
// Deprecated: true
// Responses:
// 201: Ack
// 400: ValidationError
// swagger:route POST /alertmanager/{DatasourceUID}/config/api/v1/alerts alertmanager RoutePostAlertingConfig
//
// sets an Alerting config

@ -5696,38 +5696,6 @@
"tags": [
"alertmanager"
]
},
"post": {
"deprecated": true,
"description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.",
"operationId": "RoutePostGrafanaAlertingConfig",
"parameters": [
{
"in": "body",
"name": "Body",
"schema": {
"$ref": "#/definitions/PostableUserConfig"
}
}
],
"responses": {
"201": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "sets an Alerting config",
"tags": [
"alertmanager"
]
}
},
"/alertmanager/grafana/config/api/v1/receivers": {

@ -321,38 +321,6 @@
}
}
},
"post": {
"description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.",
"tags": [
"alertmanager"
],
"summary": "sets an Alerting config",
"operationId": "RoutePostGrafanaAlertingConfig",
"deprecated": true,
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/PostableUserConfig"
}
}
],
"responses": {
"201": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.",
"tags": [

@ -1,5 +1,5 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
//
// Frame[0] {
// "type": "directory-listing",
// "typeVersion": [
@ -10,7 +10,7 @@
// "HasMore": false
// }
// }
// Name:
// Name:
// Dimensions: 3 Fields by 3 Rows
// +----------------------------+----------------------+---------------+
// | Name: name | Name: mediaType | Name: size |
@ -87,4 +87,4 @@
}
}
]
}
}

@ -10,8 +10,6 @@ import (
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
@ -354,49 +352,3 @@ func TestIntegrationAdminConfiguration_SendingToExternalAlertmanagers(t *testing
}, 16*time.Second, 8*time.Second) // the sync interval is 2s so after 8s all alertmanagers (if any) most probably are started
}
}
func TestIntegrationAdminConfiguration_CannotCreateInhibitionRules(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
cfg := apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
},
InhibitRules: []config.InhibitRule{{
SourceMatchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo",
Value: "bar",
}},
TargetMatchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "bar",
Value: "baz",
}},
}},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
}
ok, err := client.PostConfiguration(t, cfg)
require.False(t, ok)
require.EqualError(t, err, "inhibition rules are not supported")
}

@ -1,554 +0,0 @@
package alerting
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func TestIntegrationAlertmanagerConfiguration(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
cases := []struct {
name string
cfg apimodels.PostableUserConfig
expErr string
}{{
name: "configuration with default route",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with UTF-8 matchers",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
GroupBy: []model.LabelName{"foo🙂"},
Matchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}, {
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese
Value: "Shiba Inu",
}},
}},
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with UTF-8 object matchers",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
GroupBy: []model.LabelName{"foo🙂"},
ObjectMatchers: apimodels.ObjectMatchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}, {
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese
Value: "Shiba Inu",
}},
}},
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with UTF-8 in both matchers and object matchers",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
GroupBy: []model.LabelName{"foo🙂"},
Matchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}},
ObjectMatchers: apimodels.ObjectMatchers{{
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese
Value: "Shiba Inu",
}},
}},
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
// TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be
// removed before version 1.0. Remove this test when support for mute time
// intervals is removed.
name: "configuration with mute time intervals",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
MuteTimeIntervals: []string{"weekends"},
}},
},
MuteTimeIntervals: []config.MuteTimeInterval{{
Name: "weekends",
TimeIntervals: []timeinterval.TimeInterval{{
Weekdays: []timeinterval.WeekdayRange{{
InclusiveRange: timeinterval.InclusiveRange{
Begin: 1,
End: 5,
},
}},
}},
}},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with time intervals",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
MuteTimeIntervals: []string{"weekends"},
}},
},
TimeIntervals: []config.TimeInterval{{
Name: "weekends",
TimeIntervals: []timeinterval.TimeInterval{{
Weekdays: []timeinterval.WeekdayRange{{
InclusiveRange: timeinterval.InclusiveRange{
Begin: 1,
End: 5,
},
}},
}},
}},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ok, err := client.PostConfiguration(t, tc.cfg)
if tc.expErr != "" {
require.EqualError(t, err, tc.expErr)
require.False(t, ok)
} else {
require.NoError(t, err)
require.True(t, ok)
}
})
}
}
func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
NGAlertAlertmanagerConfigPollInterval: 2 * time.Second,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotatest.New(false, nil))
require.NoError(t, err)
// editor from main organisation requests configuration
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// create user under main organisation
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
// create another organisation
newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "another org", UserID: userID})
require.NoError(t, err)
orgID := newOrg.ID
// create user under different organisation
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor-42",
Login: "editor-42",
OrgID: orgID,
})
// On a blank start with no configuration, it saves and delivers the default configuration.
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
// When creating new configuration, if it fails to apply - it does not save it.
{
payload := `
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"iconEmoji": "",
"iconUrl": "",
"mentionGroups": "",
"mentionUsers": "",
"recipient": "#unified-alerting-test",
"username": ""
},
"secureSettings": {},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false,
"uid": ""
}]
}]
}
}
`
resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"])
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
// editor42 from organisation 42 posts configuration
alertConfigURL = fmt.Sprintf("http://editor-42:editor-42@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// Before we start operating, make sure we've synced this org.
require.Eventually(t, func() bool {
resp, err := http.Get(alertConfigURL) // nolint
require.NoError(t, err)
return resp.StatusCode == http.StatusOK
}, 10*time.Second, 2*time.Second)
// Post the alertmanager config.
{
mockChannel := newMockNotificationChannel(t, grafanaListedAddr)
amConfig := getAlertmanagerConfig(mockChannel.server.Addr)
postRequest(t, alertConfigURL, amConfig, http.StatusAccepted) // nolint
// Verifying that the new configuration is returned
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
b := getBody(t, resp.Body)
re := regexp.MustCompile(`"uid":"([\w|-]*)"`)
e := getExpAlertmanagerConfigFromAPI(mockChannel.server.Addr)
require.JSONEq(t, e, string(re.ReplaceAll([]byte(b), []byte(`"uid":""`))))
}
// verify that main organisation still gets the default configuration
alertConfigURL = fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
}
func TestIntegrationAlertmanagerConfigurationPersistSecrets(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
DisableFeatureToggles: []string{featuremgmt.FlagAlertingApiServer},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
generatedUID := ""
// create a new configuration that has a secret
{
payload := `
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"recipient": "#unified-alerting-test"
},
"secureSettings": {
"url": "http://averysecureurl.com/webhook"
},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false
}]
}]
}
}
`
resp := postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
require.JSONEq(t, `{"message":"configuration created"}`, getBody(t, resp.Body))
}
// Try to update a receiver with unknown UID
{
// Then, update the recipient
payload := `
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"recipient": "#unified-alerting-test-but-updated"
},
"secureFields": {
"url": true
},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false,
"uid": "invalid"
}]
}]
}
}
`
resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint
s := getBody(t, resp.Body)
var res map[string]any
require.NoError(t, json.Unmarshal([]byte(s), &res))
require.Equal(t, "unknown receiver: invalid", res["message"])
}
// The secure settings must be present
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
var c apimodels.GettableUserConfig
bb := getBody(t, resp.Body)
err := json.Unmarshal([]byte(bb), &c)
require.NoError(t, err)
m := c.GetGrafanaReceiverMap()
assert.Len(t, m, 1)
for k := range m {
generatedUID = m[k].UID
}
// Then, update the recipient
payload := fmt.Sprintf(`
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"recipient": "#unified-alerting-test-but-updated"
},
"secureFields": {
"url": true
},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false,
"uid": %q
}]
}]
}
}
`, generatedUID)
resp = postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
require.JSONEq(t, `{"message": "configuration created"}`, getBody(t, resp.Body))
}
// The secure settings must be present
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, fmt.Sprintf(`
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"uid": %q,
"name": "slack.receiver",
"type": "slack",
"disableResolveMessage": false,
"settings": {
"recipient": "#unified-alerting-test-but-updated"
},
"secureFields": {
"url": true
}
}]
}]
}
}
`, generatedUID), getBody(t, resp.Body))
}
}

@ -2,6 +2,7 @@ package alerting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
@ -16,7 +17,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
@ -32,9 +32,6 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
DisableFeatureToggles: []string{
featuremgmt.FlagAlertingApiServer,
},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
@ -63,8 +60,9 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
expBody string
}
t.Run("when creating alertmanager configuration", func(t *testing.T) {
body := `
// Create alertmanager config
cfg := apimodels.PostableUserConfig{}
amConfig := `
{
"alertmanager_config": {
"route": {
@ -85,51 +83,10 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
}
}
`
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusUnauthorized,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusForbidden,
expBody: `"title":"Access denied"`,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusAccepted,
expBody: `{"message":"configuration created"}`,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusAccepted,
expBody: `{"message":"configuration created"}`,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
url := fmt.Sprintf(tc.url, grafanaListedAddr)
buf := bytes.NewReader([]byte(body))
// nolint:gosec
resp, err := http.Post(url, "application/json", buf)
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(b), tc.expBody)
})
}
})
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg)
require.NoError(t, err)
t.Run("when retrieve alertmanager configuration", func(t *testing.T) {
cfgTemplate := `
@ -329,7 +286,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
})
var silences apimodels.GettableSilences
err := json.Unmarshal(blob, &silences)
err = json.Unmarshal(blob, &silences)
require.NoError(t, err)
assert.Len(t, silences, 2)
silenceIDs := make([]string, 0, len(silences))

@ -28,7 +28,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/infra/db"
@ -249,112 +248,6 @@ func TestIntegrationTestReceivers(t *testing.T) {
require.JSONEq(t, expectedJSON, string(b))
})
t.Run("assert working receiver with existing secure settings returns OK", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Login: "grafana",
Password: "password",
})
mockChannel := newMockNotificationChannel(t, grafanaListedAddr)
amConfig := createAlertmanagerConfig(`{
"alertmanager_config": {
"route": {
"receiver": "receiver-1"
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid": "",
"name": "receiver-1",
"type": "slack",
"disableResolveMessage": false,
"settings": {},
"secureSettings": {"url": "http://CHANNEL_ADDR/slack_recv1/slack_test_without_token"}
}
]
}]
}
}`, mockChannel.server.Addr)
// Set up responses
mockChannel.responses["slack_recv1"] = `{"ok": true}`
// Post config
u := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
_ = postRequest(t, u, amConfig, http.StatusAccepted) // nolint
// Get am config with UID and without secureSettings
resp := getRequest(t, u, http.StatusOK)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
config, err := notifier.Load(b)
require.NoError(t, err)
body, err := json.Marshal(apimodels.TestReceiversConfigBodyParams{
Receivers: config.AlertmanagerConfig.Receivers,
})
require.NoError(t, err)
// Test using the am config without secureSettings
testReceiversURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/receivers/test", grafanaListedAddr)
// nolint
resp = postRequest(t, testReceiversURL, string(body), http.StatusOK)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err = io.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test",
"__dashboardUid__": "dashboard_uid",
"__orgId__": "1",
"__panelId__": "1",
"__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]",
"__values__": "{\"B\":22,\"C\":1}"
},
"labels": {
"alertname": "TestAlert",
"grafana_folder": "Test Folder",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "ok"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
})
t.Run("assert invalid receiver returns 400 Bad Request", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
@ -1008,8 +901,11 @@ func TestIntegrationNotificationChannels(t *testing.T) {
apiClient.CreateFolder(t, "default", "default")
// Post the alertmanager config.
u := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
_ = postRequest(t, u, amConfig, http.StatusAccepted) // nolint
cfg := apimodels.PostableUserConfig{}
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg)
require.NoError(t, err)
// Verifying that all the receivers and routes have been registered.
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
@ -1025,7 +921,7 @@ func TestIntegrationNotificationChannels(t *testing.T) {
b = getBody(t, resp.Body)
var receivers []apimodels.Receiver
err := json.Unmarshal([]byte(b), &receivers)
err = json.Unmarshal([]byte(b), &receivers)
require.NoError(t, err)
for _, rcv := range receivers {
require.NotNil(t, rcv.Name)

@ -3288,7 +3288,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
"intervalSeconds":60,
"is_paused": false,
"version":1,
"uid":"uid",
"uid":"uid",
"guid": "guid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",
@ -4451,32 +4451,6 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) {
t.Log(body)
})
t.Run("create with '...' groupBy followed by config post should succeed", func(t *testing.T) {
var copyD testData
err = json.Unmarshal(testDataRaw, &copyD)
group := copyD.RuleGroup
ns := group.Rules[0].GrafanaManagedAlert.NotificationSettings
ns.GroupBy = []string{ngmodels.FolderTitleLabel, model.AlertNameLabel, ngmodels.GroupByAll}
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &group, false)
require.Equalf(t, http.StatusAccepted, status, body)
// Now update the config with no changes.
_, status, body = apiClient.GetAlertmanagerConfigWithStatus(t)
if !assert.Equalf(t, http.StatusOK, status, body) {
return
}
cfg := apimodels.PostableUserConfig{}
err = json.Unmarshal([]byte(body), &cfg)
require.NoError(t, err)
ok, err := apiClient.PostConfiguration(t, cfg)
require.NoError(t, err)
require.True(t, ok)
})
t.Run("should create rule and generate route", func(t *testing.T) {
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &d.RuleGroup, false)
require.Equalf(t, http.StatusAccepted, status, body)

@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -420,40 +419,6 @@ func (a apiClient) UpdateAlertRuleOrgQuota(t *testing.T, orgID int64, limit int6
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func (a apiClient) PostConfiguration(t *testing.T, c apimodels.PostableUserConfig) (bool, error) {
t.Helper()
b, err := json.Marshal(c)
require.NoError(t, err)
u := fmt.Sprintf("%s/api/alertmanager/grafana/config/api/v1/alerts", a.url)
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
require.NotNil(t, resp)
defer func() {
_ = resp.Body.Close()
}()
b, err = io.ReadAll(resp.Body)
require.NoError(t, err)
data := struct {
Message string `json:"message"`
}{}
require.NoError(t, json.Unmarshal(b, &data))
if resp.StatusCode == http.StatusAccepted {
return true, nil
}
return false, errors.New(data.Message)
}
func (a apiClient) PostRulesGroupWithStatus(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig, permanentlyDelete bool) (apimodels.UpdateRuleGroupResponse, int, string) {
t.Helper()
buf := bytes.Buffer{}

@ -0,0 +1,79 @@
package common
import (
"testing"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
v0alpha1_receiver "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1"
v0alpha1_routingtree "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1"
v0alpha1_templategroup "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1"
v0alpha1_timeinterval "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
)
func NewReceiverClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_receiver.Receiver, v0alpha1_receiver.ReceiverList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_receiver.Receiver, v0alpha1_receiver.ReceiverList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_receiver.Kind().Group(),
Version: v0alpha1_receiver.Kind().Version(),
Resource: v0alpha1_receiver.Kind().Plural(),
}).Namespace("default"),
}
}
func NewRoutingTreeClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_routingtree.RoutingTree, v0alpha1_routingtree.RoutingTreeList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_routingtree.RoutingTree, v0alpha1_routingtree.RoutingTreeList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_routingtree.Kind().Group(),
Version: v0alpha1_routingtree.Kind().Version(),
Resource: v0alpha1_routingtree.Kind().Plural(),
}).Namespace("default"),
}
}
func NewTemplateGroupClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_templategroup.TemplateGroup, v0alpha1_templategroup.TemplateGroupList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_templategroup.TemplateGroup, v0alpha1_templategroup.TemplateGroupList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_templategroup.Kind().Group(),
Version: v0alpha1_templategroup.Kind().Version(),
Resource: v0alpha1_templategroup.Kind().Plural(),
}).Namespace("default"),
}
}
func NewTimeIntervalClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_timeinterval.TimeInterval, v0alpha1_timeinterval.TimeIntervalList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_timeinterval.TimeInterval, v0alpha1_timeinterval.TimeIntervalList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_timeinterval.Kind().Group(),
Version: v0alpha1_timeinterval.Kind().Version(),
Resource: v0alpha1_timeinterval.Kind().Plural(),
}).Namespace("default"),
}
}

@ -14,18 +14,19 @@ import (
"testing"
"github.com/grafana/alerting/notify"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree"
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
@ -67,7 +68,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := test_common.NewReceiverClient(t, helper.Org1.Admin)
newResource := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@ -145,7 +146,7 @@ func TestIntegrationResourcePermissions(t *testing.T) {
admin := org1.Admin
viewer := org1.Viewer
editor := org1.Editor
adminClient := newClient(t, admin)
adminClient := test_common.NewReceiverClient(t, admin)
writeACMetadata := []string{"canWrite", "canDelete"}
allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin"}
@ -297,8 +298,8 @@ func TestIntegrationResourcePermissions(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
createClient := newClient(t, tc.creatingUser)
client := newClient(t, tc.testUser)
createClient := test_common.NewReceiverClient(t, tc.creatingUser)
client := test_common.NewReceiverClient(t, tc.testUser)
var created = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
@ -561,10 +562,10 @@ func TestIntegrationAccessControl(t *testing.T) {
},
}
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := test_common.NewReceiverClient(t, tc.user)
var expected = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
@ -739,7 +740,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
// Prepare environment and create notification policy and rule that use receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
@ -754,7 +755,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
parentRoute.Routes = []*definitions.Route{&route1, &route2}
amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, &parentRoute)
persistInitialConfig(t, amConfig, adminClient, legacyCli)
persistInitialConfig(t, amConfig)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
@ -817,8 +818,11 @@ func TestIntegrationInUseMetadata(t *testing.T) {
// Removing the new extra route should leave only 1.
amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1]
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
_, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{})
require.NoError(t, err)
receiverListed, receiverGet = requestReceivers(t, "user-defined")
checkInUse(t, receiverListed, receiverGet, 1, 2)
@ -834,11 +838,14 @@ func TestIntegrationInUseMetadata(t *testing.T) {
receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
checkInUse(t, receiverListed, receiverGet, 1, 0)
// Remove the rest.
// Remove the remaining routes.
amConfig.AlertmanagerConfig.Route.Routes = nil
success, err = legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
// Remove the remaining rules.
ruleGroup.Rules = nil
_, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
@ -861,7 +868,7 @@ func TestIntegrationProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
@ -915,7 +922,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@ -998,7 +1005,7 @@ func TestIntegrationPatch(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@ -1086,75 +1093,6 @@ func TestIntegrationPatch(t *testing.T) {
})
}
func TestIntegrationRejectConfigApiReceiverModification(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := getTestHelper(t)
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := newClient(t, helper.Org1.Admin)
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
persistInitialConfig(t, amConfig, adminClient, legacyCli)
// We modify the receiver, this should cause the POST to be rejected.
userDefinedReceiver := amConfig.AlertmanagerConfig.Receivers[slices.IndexFunc(amConfig.AlertmanagerConfig.Receivers, func(receiver *definitions.PostableApiReceiver) bool {
return receiver.Name == "user-defined"
})]
userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage = !userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Falsef(t, success, "Expected receiver modification to be rejected, but got %t", success)
require.ErrorContainsf(t, err, "alertingApiServer", "Expected error message to contain 'alertingApiServer', but got %s", err)
// Revert the change.
userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage = !userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage
// We add a receiver, this should be accepted.
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, &definitions.PostableApiReceiver{
Receiver: config.Receiver{
Name: "new receiver",
},
PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
{
Name: "new receiver",
Type: "email",
Settings: []byte(`{"addresses": "<some@email.com>"}`),
},
},
},
})
success, err = legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Expected receiver modification to be accepted, but got %s", err)
require.NoError(t, err)
// Sanity check.
gettable, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t)
require.Equalf(t, http.StatusOK, status, body)
require.Lenf(t, gettable.AlertmanagerConfig.Receivers, 3, "Expected 3 receivers, got %d", len(gettable.AlertmanagerConfig.Receivers))
// We remove the receiver, this should be accepted.
amConfig.AlertmanagerConfig.Receivers = slices.DeleteFunc(amConfig.AlertmanagerConfig.Receivers, func(receiver *definitions.PostableApiReceiver) bool {
return receiver.GrafanaManagedReceivers[0].Name == "new receiver"
})
success, err = legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Expected receiver modification to be accepted, but got %s", err)
require.NoError(t, err)
// Sanity check.
gettable, status, body = legacyCli.GetAlertmanagerConfigWithStatus(t)
require.Equalf(t, http.StatusOK, status, body)
require.Lenf(t, gettable.AlertmanagerConfig.Receivers, 2, "Expected 2 receivers, got %d", len(gettable.AlertmanagerConfig.Receivers))
}
func TestIntegrationReferentialIntegrity(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@ -1171,14 +1109,14 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
// Prepare environment and create notification policy and rule that use time receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
persistInitialConfig(t, amConfig, adminClient, legacyCli)
persistInitialConfig(t, amConfig)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
@ -1283,7 +1221,7 @@ func TestIntegrationCRUD(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
var defaultReceiver *v0alpha1.Receiver
t.Run("should list the default receiver", func(t *testing.T) {
items, err := adminClient.List(ctx, v1.ListOptions{})
@ -1451,7 +1389,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
recv1 := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@ -1533,13 +1471,14 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
// persistInitialConfig helps create an initial config with new receivers using legacy json. Config API blocks receiver
// modifications, so we need to use k8s API to create new receivers before posting the config.
func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig, adminClient *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList], legacyCli alerting.LegacyApiClient) {
func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) {
ctx := context.Background()
var defaultReceiver *definitions.PostableApiReceiver
helper := getTestHelper(t)
receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
for _, receiver := range amConfig.AlertmanagerConfig.Receivers {
if receiver.Name == "grafana-default-email" {
defaultReceiver = receiver
continue
}
@ -1563,7 +1502,7 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig,
})
}
created, err := adminClient.Create(ctx, &toCreate, v1.CreateOptions{})
created, err := receiverClient.Create(ctx, &toCreate, v1.CreateOptions{})
require.NoError(t, err)
for i, integration := range created.Spec.Integrations {
@ -1571,19 +1510,13 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig,
}
}
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
gettable, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t)
require.Equalf(t, http.StatusOK, status, body)
idx := slices.IndexFunc(gettable.AlertmanagerConfig.Receivers, func(recv *definitions.GettableApiReceiver) bool {
return recv.Name == "grafana-default-email"
})
gettableDefault := gettable.AlertmanagerConfig.Receivers[idx]
nsMapper := func(_ int64) string { return "default" }
// Assign uid of default receiver as well.
defaultReceiver.GrafanaManagedReceivers[0].UID = gettableDefault.GrafanaManagedReceivers[0].UID
routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper)
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
}
func createIntegration(t *testing.T, integrationType string) v0alpha1.Integration {
@ -1609,19 +1542,3 @@ func createWildcardPermission(actions ...string) resourcepermissions.SetResource
ResourceID: "*",
}
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
}

@ -8,7 +8,6 @@ import (
"testing"
"time"
"github.com/grafana/alerting/definition"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
@ -16,10 +15,12 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1"
v0alpha1_timeinterval "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@ -32,6 +33,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@ -52,7 +54,7 @@ func TestIntegrationNotAllowedMethods(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
route := &v0alpha1.RoutingTree{
ObjectMeta: v1.ObjectMeta{
@ -156,11 +158,11 @@ func TestIntegrationAccessControl(t *testing.T) {
}
admin := org1.Admin
adminClient := newClient(t, admin)
adminClient := common.NewRoutingTreeClient(t, admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := common.NewRoutingTreeClient(t, tc.user)
if tc.canRead {
t.Run("should be able to list routing trees", func(t *testing.T) {
@ -291,7 +293,7 @@ func TestIntegrationProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, admin)
adminClient := common.NewRoutingTreeClient(t, admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
@ -342,7 +344,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
@ -388,46 +390,30 @@ func TestIntegrationDataConsistency(t *testing.T) {
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
client := newClient(t, helper.Org1.Admin)
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
receiver := "grafana-default-email"
timeInterval := "test-time-interval"
createRoute := func(t *testing.T, route definitions.Route) {
t.Helper()
cfg, _, _ := legacyCli.GetAlertmanagerConfigWithStatus(t)
var receivers []*definitions.PostableApiReceiver
for _, apiReceiver := range cfg.AlertmanagerConfig.Receivers {
var recv []*definitions.PostableGrafanaReceiver
for _, r := range apiReceiver.GrafanaManagedReceivers {
recv = append(recv, &definitions.PostableGrafanaReceiver{
UID: r.UID,
Name: r.Name,
Type: r.Type,
DisableResolveMessage: r.DisableResolveMessage,
Settings: r.Settings,
})
}
receivers = append(receivers, &definitions.PostableApiReceiver{
Receiver: config.Receiver{Name: apiReceiver.Name},
PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{GrafanaManagedReceivers: recv},
})
}
_, err := legacyCli.PostConfiguration(t, definitions.PostableUserConfig{
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
Config: definition.Config{
Route: &route,
TimeIntervals: []config.TimeInterval{
{
Name: timeInterval,
},
},
},
Receivers: receivers,
},
})
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1Route, v1.UpdateOptions{})
require.NoError(t, err)
}
_, err := common.NewTimeIntervalClient(t, helper.Org1.Admin).Create(ctx, &v0alpha1_timeinterval.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1_timeinterval.Spec{
Name: timeInterval,
TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(1),
},
}, v1.CreateOptions{})
require.NoError(t, err)
var regex config.Regexp
require.NoError(t, json.Unmarshal([]byte(`".*"`), &regex))
@ -461,7 +447,7 @@ func TestIntegrationDataConsistency(t *testing.T) {
createRoute(t, route)
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, []v0alpha1.Matcher{
expected := []v0alpha1.Matcher{
{
Label: "label_match",
Type: v0alpha1.MatcherTypeEqual,
@ -482,7 +468,8 @@ func TestIntegrationDataConsistency(t *testing.T) {
Type: v0alpha1.MatcherTypeNotEqualRegex,
Value: "test-456",
},
}, tree.Spec.Routes[0].Matchers)
}
assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers)
})
t.Run("should save into ObjectMatchers", func(t *testing.T) {
route := definitions.Route{
@ -623,20 +610,55 @@ func TestIntegrationDataConsistency(t *testing.T) {
require.Equalf(t, http.StatusOK, status, body)
require.Equal(t, before, after)
})
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
t.Run("unicode support in groupBy and matchers", func(t *testing.T) {
route := definitions.Route{
Receiver: receiver,
Routes: []*definitions.Route{
{
GroupByStr: []string{"foo🙂"},
Matchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}},
ObjectMatchers: definitions.ObjectMatchers{{
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese (inu)
Value: "Shiba Inu",
}},
},
},
}
return &apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
createRoute(t, route)
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "foo🙂", tree.Spec.Routes[0].GroupBy[0])
expected := []v0alpha1.Matcher{
{Label: "foo🙂", Type: v0alpha1.MatcherTypeEqual, Value: "bar"},
{Label: "_bar1", Type: v0alpha1.MatcherTypeNotEqual, Value: "baz🙂"},
{Label: "0baz", Type: v0alpha1.MatcherTypeEqualRegex, Value: "[a-zA-Z0-9]+,?"},
{Label: "corge", Type: v0alpha1.MatcherTypeNotEqualRegex, Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$"},
{Label: "Προμηθέας", Type: v0alpha1.MatcherTypeEqual, Value: "Prom"},
{Label: "犬", Type: v0alpha1.MatcherTypeNotEqual, Value: "Shiba Inu"},
}
assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers)
})
}

@ -11,9 +11,7 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1"
"github.com/grafana/grafana/pkg/bus"
@ -27,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@ -47,7 +46,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
newTemplate := &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@ -217,11 +216,11 @@ func TestIntegrationAccessControl(t *testing.T) {
},
}
adminClient := newClient(t, org1.Admin)
adminClient := common.NewTemplateGroupClient(t, org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := common.NewTemplateGroupClient(t, tc.user)
var expected = &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@ -377,7 +376,7 @@ func TestIntegrationProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, admin)
adminClient := common.NewTemplateGroupClient(t, admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
@ -427,7 +426,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
template := v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@ -511,7 +510,7 @@ func TestIntegrationPatch(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
template := v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@ -571,7 +570,7 @@ func TestIntegrationListSelector(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
template1 := &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@ -664,19 +663,3 @@ func TestIntegrationListSelector(t *testing.T) {
require.NotEqualf(t, templates.DefaultTemplateName, list.Items[1].Name, "Expected non-default template but got %s", list.Items[1].Name)
})
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
}

@ -15,14 +15,14 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@ -57,7 +58,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := common.NewTimeIntervalClient(t, helper.Org1.Admin)
newInterval := &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@ -191,11 +192,11 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
},
}
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := common.NewTimeIntervalClient(t, tc.user)
var expected = &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@ -349,7 +350,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
@ -401,7 +402,7 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
interval := v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@ -485,7 +486,7 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
interval := v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@ -550,7 +551,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
interval1 := &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@ -650,8 +651,26 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
mtis := []definitions.MuteTimeInterval{}
for _, interval := range amConfig.AlertmanagerConfig.MuteTimeIntervals {
mtis = append(mtis, definitions.MuteTimeInterval{
MuteTimeInterval: interval,
})
}
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
v1intervals, err := timeinterval.ConvertToK8sResources(orgID, mtis, func(int64) string { return "default" }, nil)
require.NoError(t, err)
for _, interval := range v1intervals.Items {
_, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
require.NoError(t, err)
}
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
@ -667,8 +686,6 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
require.Equal(t, http.StatusAccepted, status)
adminClient := newClient(t, helper.Org1.Admin)
intervals, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, intervals.Items, 2)
@ -769,7 +786,7 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
testCases := []struct {
name string
@ -809,19 +826,3 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) {
})
}
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
}

@ -4,7 +4,6 @@ import { Route, Routes } from 'react-router-dom-v5-compat';
import { render, screen, userEvent, within } from 'test/test-utils';
import { byTestId } from 'testing-library-selector';
import { config } from '@grafana/runtime';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
@ -19,6 +18,7 @@ import { AccessControlAction } from 'app/types';
import EditMuteTimingPage from './components/mute-timings/EditMuteTiming';
import NewMuteTimingPage from './components/mute-timings/NewMuteTiming';
import { defaultConfig, muteTimeInterval } from './components/mute-timings/mocks';
import { grantUserPermissions, mockDataSource } from './mocks';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
@ -59,21 +59,6 @@ const ui = {
years: byTestId('mute-timing-years'),
};
const muteTimeInterval: MuteTimeInterval = {
name: 'default-mute',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
};
const muteTimeInterval2: MuteTimeInterval = {
name: 'default-mute2',
time_intervals: [
@ -90,26 +75,6 @@ const muteTimeInterval2: MuteTimeInterval = {
],
};
/** Alertmanager config where time intervals are stored in `mute_time_intervals` property */
export const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
mute_time_intervals: [muteTimeInterval],
},
template_files: {},
};
/** Alertmanager config where time intervals are stored in `time_intervals` property */
const defaultConfigWithNewTimeIntervalsField: AlertManagerCortexConfig = {
alertmanager_config: {
@ -200,6 +165,7 @@ describe('Mute timings', () => {
grantUserPermissions(Object.values(AccessControlAction));
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
setAlertmanagerConfig(dataSources.am.uid, defaultConfig);
// TODO: Add this at a higher level to ensure that no tests depend on others running first
// Without this, the selected alertmanager in a previous test can affect the next, meaning tests
@ -209,7 +175,7 @@ describe('Mute timings', () => {
it('creates a new mute timing, with mute_time_intervals in config', async () => {
const capture = captureRequests();
renderMuteTimings('/alerting/routes/new');
renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` });
await screen.findByText(/add mute timing/i);
@ -276,10 +242,9 @@ describe('Mute timings', () => {
it('prepopulates the form when editing a mute timing', async () => {
const capture = captureRequests();
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}`,
search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}&alertmanager=${dataSources.am.name}`,
});
expect(await ui.nameField.find()).toBeInTheDocument();
@ -318,7 +283,7 @@ describe('Mute timings', () => {
});
it('form is invalid with duplicate mute timing name', async () => {
renderMuteTimings('/alerting/routes/new');
renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` });
await fillOutForm({ name: muteTimeInterval.name, days: '1' });
@ -330,7 +295,7 @@ describe('Mute timings', () => {
it('replaces mute timings in routes when the mute timing name is changed', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}`,
search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}&alertmanager=${dataSources.am.name}`,
});
expect(await ui.nameField.find()).toBeInTheDocument();
@ -352,46 +317,40 @@ describe('Mute timings', () => {
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
});
describe('alertingApiServer feature toggle', () => {
beforeEach(() => {
config.featureToggles.alertingApiServer = true;
});
it('allows creation of new mute timings', async () => {
renderMuteTimings('/alerting/routes/new');
it('allows creation of new mute timings', async () => {
renderMuteTimings('/alerting/routes/new');
await fillOutForm({ name: 'a new mute timing' });
await fillOutForm({ name: 'a new mute timing' });
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
it('shows error when mute timing does not exist', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`,
});
it('shows error when mute timing does not exist', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`,
});
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
});
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
it('loads edit form correctly and allows saving', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH}`,
});
it('loads edit form correctly and allows saving', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH}`,
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
it('loads view form for provisioned interval', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`,
});
it('loads view form for provisioned interval', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`,
});
expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument();
});
expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument();
});
});

@ -21,7 +21,6 @@ import {
TIME_INTERVAL_NAME_FILE_PROVISIONED,
TIME_INTERVAL_NAME_HAPPY_PATH,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import {
AlertManagerCortexConfig,
@ -140,13 +139,7 @@ const getRootRoute = async () => {
return ui.rootRouteContainer.find();
};
describe.each([
// k8s API enabled
true,
// k8s API disabled
false,
])('NotificationPolicies with alertingApiServer=%p', (apiServerEnabled) => {
apiServerEnabled ? testWithFeatureToggles(['alertingApiServer']) : testWithFeatureToggles([]);
describe('NotificationPolicies', () => {
beforeEach(() => {
setupDataSources(...Object.values(dataSources));
grantUserPermissions([
@ -369,22 +362,6 @@ describe.each([
});
});
describe('Grafana alertmanager - config API', () => {
it('Converts matchers to object_matchers for grafana alertmanager', async () => {
const { user } = renderNotificationPolicies();
const policyIndex = 0;
await openEditModal(policyIndex);
// Save policy to test that format is converted to object_matchers
await user.click(await ui.saveButton.find());
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
const updatedConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].object_matchers).toMatchSnapshot();
});
});
describe('Non-Grafana alertmanagers', () => {
it.skip('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => {
makeAllAlertmanagerConfigFetchFail(getErrorResponse('alertmanager storage object not found'));

@ -8,7 +8,6 @@ import { byLabelText, byRole } from 'testing-library-selector';
import { CodeEditor } from '@grafana/ui';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { AccessControlAction } from 'app/types';
import Templates from './Templates';
@ -76,15 +75,18 @@ const setup = (initialEntries: InitialEntry[]) => {
);
};
const slackTemplate = 'k8s-template%20with%20spaces-resource-name';
const emailTemplate = 'k8s-custom-email-resource-name';
describe('Templates routes', () => {
it('allows duplication of template with spaces in name', async () => {
setup([navUrl.duplicate('template%20with%20spaces')]);
setup([navUrl.duplicate(slackTemplate)]);
expect(await screen.findByText('Edit payload')).toBeInTheDocument();
});
it('allows editing of template with spaces in name', async () => {
setup([navUrl.edit('template%20with%20spaces')]);
setup([navUrl.edit(slackTemplate)]);
expect(await screen.findByText('Edit payload')).toBeInTheDocument();
});
@ -99,7 +101,7 @@ describe('Templates routes', () => {
});
it('should pass name validation when editing existing template', async () => {
const { user } = setup([navUrl.edit('custom-email')]);
const { user } = setup([navUrl.edit(emailTemplate)]);
const titleElement = await ui.form.title.find();
await waitFor(() => {
@ -115,21 +117,6 @@ describe('Templates routes', () => {
});
});
it('should display error message when creating new template with duplicate name', async () => {
const { user } = setup([navUrl.new]);
const titleElement = await ui.form.title.find();
await user.type(titleElement, 'custom-email');
await user.click(ui.form.saveButton.get());
expect(screen.getByText('Another template with this name already exists')).toBeInTheDocument();
});
});
describe('Templates K8s API', () => {
testWithFeatureToggles(['alertingApiServer']);
it('form edit renders with correct form values', async () => {
setup([navUrl.edit('k8s-custom-email-resource-name')]);

@ -1,30 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Grafana alertmanager - config API Converts matchers to object_matchers for grafana alertmanager 1`] = `
[
[
"sub1matcher1",
"=",
"sub1value1",
],
[
"sub1matcher2",
"=",
"sub1value2",
],
[
"sub1matcher3",
"=~",
"sub1value3",
],
[
"sub1matcher4",
"=~",
"sub1value4",
],
]
`;
exports[`Non-Grafana alertmanagers Keeps matchers for non-grafana alertmanager sources 1`] = `
[
"hello="world"",

@ -67,7 +67,6 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
* Used to determine whether to show the "Unused" badge
*/
const isReferencedByAnything = usingK8sApi ? Boolean(numberOfPolicies || numberOfRules) : policies.length > 0;
/** Does the current user have permissions to edit the contact point? */
const hasAbilityToEdit = canEditEntity(contactPoint) || editAllowed;
/** Can the contact point actually be edited via the UI? */

@ -4,7 +4,7 @@ import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within }
import { selectors } from '@grafana/e2e-selectors';
import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
import { flushMicrotasks, testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { flushMicrotasks } from 'app/features/alerting/unified/test/test-utils';
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
@ -60,6 +60,16 @@ const basicContactPoint: ContactPointWithMetadata = {
grafana_managed_receiver_configs: [],
};
const basicContactPointInUse: ContactPointWithMetadata = {
...basicContactPoint,
metadata: {
annotations: {
[K8sAnnotations.InUseRules]: '1',
[K8sAnnotations.InUseRoutes]: '1',
},
},
};
const contactPointWithEverything: ContactPointWithMetadata = {
...basicContactPoint,
metadata: {
@ -171,9 +181,8 @@ describe('contact points', () => {
expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'export all' })).toBeInTheDocument();
// 2 of them are unused by routes in the mock response
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(3);
expect(unusedBadge).toHaveLength(4);
const viewProvisioned = screen.getByRole('link', { name: 'view-action' });
expect(viewProvisioned).toBeInTheDocument();
@ -203,25 +212,13 @@ describe('contact points', () => {
// should disable create contact point
expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true');
// there should be no edit buttons
expect(screen.queryAllByRole('link', { name: 'edit-action' })).toHaveLength(0);
// edit permission is based on API response - we should have 3 buttons
const editButtons = await screen.findAllByRole('link', { name: 'edit-action' });
expect(editButtons).toHaveLength(3);
// there should be view buttons though
// there should be view buttons though - one for provisioned, and one for the un-editable contact point
const viewButtons = screen.getAllByRole('link', { name: 'view-action' });
expect(viewButtons).toHaveLength(5);
// delete should be disabled in the "more" actions
const moreButtons = screen.queryAllByRole('button', { name: /More/ });
expect(moreButtons).toHaveLength(5);
// check if all of the delete buttons are disabled
for await (const button of moreButtons) {
await user.click(button);
const deleteButton = screen.queryByRole('menuitem', { name: 'delete' });
expect(deleteButton).toBeDisabled();
// click outside the menu to close it otherwise we can't interact with the rest of the page
await user.click(document.body);
}
expect(viewButtons).toHaveLength(2);
// check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' });
@ -297,7 +294,7 @@ describe('contact points', () => {
},
];
const { user } = renderWithProvider(<ContactPoint contactPoint={{ ...basicContactPoint, policies }} />);
const { user } = renderWithProvider(<ContactPoint contactPoint={{ ...basicContactPointInUse, policies }} />);
expect(screen.getByRole('link', { name: /1 notification policy/ })).toBeInTheDocument();
@ -431,9 +428,7 @@ describe('contact points', () => {
});
});
describe('alertingApiServer enabled', () => {
testWithFeatureToggles(['alertingApiServer']);
describe('Grafana alertmanager', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,

@ -1,7 +1,6 @@
import { render, screen, within } from 'test/test-utils';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
@ -74,44 +73,40 @@ describe('NotificationTemplates', () => {
expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument();
});
describe('k8s API', () => {
testWithFeatureToggles(['alertingApiServer']);
it('Should render templates table with the correct rows', async () => {
renderWithProvider();
it('Should render templates table with the correct rows', async () => {
renderWithProvider();
const slackRow = await screen.findByRole('row', { name: /slack-template/i });
expect(within(slackRow).getByRole('cell', { name: /slack-template/i })).toBeInTheDocument();
const slackRow = await screen.findByRole('row', { name: /slack-template/i });
expect(within(slackRow).getByRole('cell', { name: /slack-template/i })).toBeInTheDocument();
const emailRow = await screen.findByRole('row', { name: /custom-email/i });
expect(within(emailRow).getByRole('cell', { name: /custom-email/i })).toBeInTheDocument();
const emailRow = await screen.findByRole('row', { name: /custom-email/i });
expect(within(emailRow).getByRole('cell', { name: /custom-email/i })).toBeInTheDocument();
const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i });
expect(within(provisionedRow).getByRole('cell', { name: /provisioned-template/i })).toBeInTheDocument();
});
const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i });
expect(within(provisionedRow).getByRole('cell', { name: /provisioned-template/i })).toBeInTheDocument();
});
it('Should provisioned badge for provisioned template', async () => {
renderWithProvider();
it('Should provisioned badge for provisioned template', async () => {
renderWithProvider();
const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i });
expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument();
});
const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i });
expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument();
});
it('Should delete template', async () => {
const { user } = renderWithProvider();
it('Should delete template', async () => {
const { user } = renderWithProvider();
const emailRow = await screen.findByRole('row', { name: /custom-email/i });
const deleteEmailButton = within(emailRow).getByRole('button', { name: /delete template/i });
const emailRow = await screen.findByRole('row', { name: /custom-email/i });
const deleteEmailButton = within(emailRow).getByRole('button', { name: /delete template/i });
await user.click(deleteEmailButton);
await user.click(deleteEmailButton);
const confirmDeleteButton = await screen.findByRole('button', { name: /Yes, delete/i });
await user.click(confirmDeleteButton);
const confirmDeleteButton = await screen.findByRole('button', { name: /Yes, delete/i });
await user.click(confirmDeleteButton);
expect(screen.queryByRole('row', { name: /custom-email/i })).not.toBeInTheDocument();
expect(await screen.findByRole('status', { name: 'Template deleted' })).toHaveTextContent(
'Template custom-email has been deleted'
);
});
expect(screen.queryByRole('row', { name: /custom-email/i })).not.toBeInTheDocument();
expect(await screen.findByRole('status', { name: 'Template deleted' })).toHaveTextContent(
'Template custom-email has been deleted'
);
});
});

@ -30,6 +30,17 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"id": "grafana-default-email",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "false",
"grafana.com/access/canWrite": "false",
"grafana.com/inUse/routes": "1",
"grafana.com/inUse/rules": "1",
"grafana.com/provenance": "none",
},
"uid": "grafana-default-email",
},
"name": "grafana-default-email",
"policies": [
{
@ -69,6 +80,17 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"id": "lotsa-emails",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none",
},
"uid": "lotsa-emails",
},
"name": "lotsa-emails",
"policies": [],
"provisioned": false,
@ -94,6 +116,17 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"id": "OnCall Conctact point",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none",
},
"uid": "OnCall Conctact point",
},
"name": "OnCall Conctact point",
"policies": [],
"provisioned": false,
@ -125,6 +158,17 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"id": "provisioned-contact-point",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "api",
},
"uid": "provisioned-contact-point",
},
"name": "provisioned-contact-point",
"policies": [
{
@ -186,6 +230,17 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"id": "Slack with multiple channels",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none",
},
"uid": "Slack with multiple channels",
},
"name": "Slack with multiple channels",
"policies": [],
"provisioned": false,
@ -226,6 +281,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
},
],
"id": "grafana-default-email",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "false",
"grafana.com/access/canWrite": "false",
"grafana.com/inUse/routes": "1",
"grafana.com/inUse/rules": "1",
"grafana.com/provenance": "none",
},
"uid": "grafana-default-email",
},
"name": "grafana-default-email",
"policies": [
{
@ -265,6 +331,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
},
],
"id": "lotsa-emails",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none",
},
"uid": "lotsa-emails",
},
"name": "lotsa-emails",
"policies": [],
"provisioned": false,
@ -293,6 +370,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
},
],
"id": "OnCall Conctact point",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none",
},
"uid": "OnCall Conctact point",
},
"name": "OnCall Conctact point",
"policies": [],
"provisioned": false,
@ -324,6 +412,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
},
],
"id": "provisioned-contact-point",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "api",
},
"uid": "provisioned-contact-point",
},
"name": "provisioned-contact-point",
"policies": [
{
@ -385,6 +484,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
},
],
"id": "Slack with multiple channels",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none",
},
"uid": "Slack with multiple channels",
},
"name": "Slack with multiple channels",
"policies": [],
"provisioned": false,

@ -2,7 +2,6 @@ import { renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { getWrapper } from 'test/test-utils';
import { config } from '@grafana/runtime';
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
@ -21,8 +20,7 @@ const wrapper = ({ children }: { children: ReactNode }) => {
setupMswServer();
const getHookResponse = async (featureToggleEnabled: boolean) => {
config.featureToggles.alertingApiServer = featureToggleEnabled;
const getHookResponse = async () => {
const { result } = renderHook(
() =>
useContactPointsWithStatus({
@ -61,41 +59,13 @@ describe('useContactPoints', () => {
it('should return contact points with status', async () => {
disablePlugin(SupportedPlugin.OnCall);
const snapshot = await getHookResponse(false);
const snapshot = await getHookResponse();
expect(snapshot).toMatchSnapshot();
});
it('returns ~matching responses with and without alertingApiServer', async () => {
// Compare the responses between the two implementations, but do not consider:
// - ID: k8s API will return id properties, but the AM config will fall back to the name of the contact point.
// These will be different, so we don't want to compare them
// - Metadata: k8s API includes metadata, AM config does not
const snapshotAmConfig = await getHookResponse(false);
const snapshotAlertingApiServer = await getHookResponse(true);
const amContactPoints = snapshotAmConfig.contactPoints.map((receiver) => {
const { id, ...rest } = receiver;
return rest;
});
const k8sContactPoints = snapshotAlertingApiServer.contactPoints.map((receiver) => {
const { id, metadata, ...rest } = receiver;
return rest;
});
expect({
...snapshotAmConfig,
contactPoints: amContactPoints,
}).toEqual({
...snapshotAlertingApiServer,
contactPoints: k8sContactPoints,
});
});
describe('when having oncall plugin installed and no alert manager config data', () => {
it('should return contact points with oncall metadata', async () => {
const snapshot = await getHookResponse(false);
const snapshot = await getHookResponse();
expect(snapshot).toMatchSnapshot();
});
});

@ -1,12 +1,9 @@
import { render, screen, userEvent, within } from 'test/test-utils';
import { config } from '@grafana/runtime';
import { defaultConfig } from 'app/features/alerting/unified/MuteTimings.test';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setMuteTimingsListError } from 'app/features/alerting/unified/mocks/server/configure';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { grantUserPermissions } from '../../mocks';
@ -15,8 +12,9 @@ import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { MuteTimingsTable } from './MuteTimingsTable';
import { defaultConfig } from './mocks';
const renderWithProvider = (alertManagerSource?: string) => {
const renderWithProvider = (alertManagerSource = GRAFANA_RULES_SOURCE_NAME) => {
return render(
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertManagerSource}>
<MuteTimingsTable />
@ -27,10 +25,15 @@ const renderWithProvider = (alertManagerSource?: string) => {
setupMswServer();
describe('MuteTimingsTable', () => {
beforeEach(() => {
window.localStorage.clear();
// setupDataSources();
});
describe('with necessary permissions', () => {
beforeEach(() => {
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
config.featureToggles.alertingApiServer = false;
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
@ -46,11 +49,10 @@ describe('MuteTimingsTable', () => {
});
it("shows individual 'export' drawer when allowed and supported, and can close", async () => {
const user = userEvent.setup();
renderWithProvider();
const { user } = renderWithProvider();
const table = await screen.findByTestId('dynamic-table');
const exportMuteTiming = await within(table).findByText(/export/i);
await user.click(exportMuteTiming);
const exportMuteTiming = await within(table).findAllByText(/export/i);
await user.click(exportMuteTiming[0]);
expect(await screen.findByRole('dialog', { name: /drawer title export/i })).toBeInTheDocument();
@ -64,25 +66,6 @@ describe('MuteTimingsTable', () => {
expect(screen.queryByRole('button', { name: /export all/i })).not.toBeInTheDocument();
});
it('deletes interval', async () => {
// TODO: Don't use captureRequests for this, move to stateful mock server instead
// and check that the interval is no longer in the list
const capture = captureRequests();
const user = userEvent.setup();
renderWithProvider();
await user.click((await screen.findAllByText(/delete/i))[0]);
await user.click(await screen.findByRole('button', { name: /delete/i }));
const requests = await capture;
const amConfigUpdateRequest = requests.find(
(r) => r.url.includes('/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST'
);
const body: AlertManagerCortexConfig = await amConfigUpdateRequest?.clone().json();
expect(body.alertmanager_config.mute_time_intervals).toHaveLength(0);
});
it('allow cancelling deletion', async () => {
// TODO: Don't use captureRequests for this, move to stateful mock server instead
// and check that the interval is still in the list
@ -100,34 +83,9 @@ describe('MuteTimingsTable', () => {
expect(amConfigUpdateRequest).toBeUndefined();
});
});
describe('without necessary permissions', () => {
beforeEach(() => {
grantUserPermissions([]);
});
it('does not show export button when not allowed ', async () => {
renderWithProvider();
expect(screen.queryByRole('button', { name: /export all/i })).not.toBeInTheDocument();
});
});
describe('using alertingApiServer feature toggle', () => {
beforeEach(() => {
config.featureToggles.alertingApiServer = true;
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
]);
});
afterEach(() => {
config.featureToggles.alertingApiServer = false;
});
it('shows list of intervals from k8s API', async () => {
renderWithProvider();
it('shows list of intervals from API', async () => {
renderWithProvider(GRAFANA_RULES_SOURCE_NAME);
expect(await screen.findByTestId('dynamic-table')).toBeInTheDocument();
expect(await screen.findByText('Provisioned')).toBeInTheDocument();
@ -157,4 +115,15 @@ describe('MuteTimingsTable', () => {
expect(deleteRequest).toBeDefined();
});
});
describe('without necessary permissions', () => {
beforeEach(() => {
grantUserPermissions([]);
});
it('does not show export button when not allowed ', async () => {
renderWithProvider();
expect(screen.queryByRole('button', { name: /export all/i })).not.toBeInTheDocument();
});
});
});

@ -0,0 +1,36 @@
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
export const muteTimeInterval: MuteTimeInterval = {
name: 'default-mute',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
};
/** Alertmanager config where time intervals are stored in `mute_time_intervals` property */
export const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
mute_time_intervals: [muteTimeInterval],
},
template_files: {},
};

@ -2,13 +2,12 @@ import { Route, Routes } from 'react-router-dom-v5-compat';
import { render, screen } from 'test/test-utils';
import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector';
import { makeAlertmanagerConfigUpdateFail } from 'app/features/alerting/unified/mocks/server/configure';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
import { testWithFeatureToggles } from '../../test/test-utils';
import { makeAllK8sEndpointsFail } from '../../mocks/server/configure';
import NewReceiverView from './NewReceiverView';
@ -33,9 +32,7 @@ beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]);
});
describe('alerting API server enabled', () => {
testWithFeatureToggles(['alertingApiServer']);
describe('new receiver', () => {
it('can create a receiver', async () => {
const { user } = renderForm();
@ -52,9 +49,7 @@ describe('alerting API server enabled', () => {
expect(await screen.findByText(/redirected/i)).toBeInTheDocument();
});
});
describe('alerting API server disabled', () => {
it('should be able to test and save a receiver', async () => {
const capture = captureRequests();
@ -90,23 +85,16 @@ describe('alerting API server disabled', () => {
const requests = await capture;
const testRequest = requests.find((r) => r.url.endsWith('/config/api/v1/receivers/test'));
const saveRequest = requests.find(
(r) => r.url.endsWith('/api/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST'
);
const saveRequest = requests.find((r) => r.url.endsWith('/receivers') && r.method === 'POST');
const testBody = await testRequest?.json();
const fullSaveBody = await saveRequest?.json();
// Only snapshot and check the receivers, as we don't want other tests to break this
// just because we added something new to the mock config
const saveBody = fullSaveBody.alertmanager_config.receivers;
const saveBody = await saveRequest?.json();
expect([testBody]).toMatchSnapshot();
expect([saveBody]).toMatchSnapshot();
});
it('does not redirect when creating contact point and API errors', async () => {
makeAlertmanagerConfigUpdateFail();
const { user } = renderForm();
await user.type(await ui.inputs.name.find(), 'receiver that should fail');
@ -114,6 +102,8 @@ describe('alerting API server disabled', () => {
await user.clear(email);
await user.type(email, 'tester@grafana.com');
makeAllK8sEndpointsFail('someerror');
await user.click(ui.saveContactButton.get());
expect(screen.queryByText(/redirected/i)).not.toBeInTheDocument();

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`alerting API server disabled should be able to test and save a receiver 1`] = `
exports[`new receiver should be able to test and save a receiver 1`] = `
[
{
"alert": {
@ -32,108 +32,16 @@ exports[`alerting API server disabled should be able to test and save a receiver
]
`;
exports[`alerting API server disabled should be able to test and save a receiver 2`] = `
exports[`new receiver should be able to test and save a receiver 2`] = `
[
[
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "grafana-default-email",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "xeKQrBrnk",
},
],
"name": "grafana-default-email",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "provisioned-contact-point",
"provenance": "api",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "s8SdCVjnk",
},
],
"name": "provisioned-contact-point",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "lotsa-emails",
"secureFields": {},
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"message": "{{ template "slack-template" . }}",
"singleEmail": false,
"subject": "some custom value",
},
"type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
},
],
"name": "lotsa-emails",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts",
},
"type": "slack",
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
},
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts2",
},
"type": "slack",
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
},
],
"name": "Slack with multiple channels",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Oncall-integration",
"settings": {
"url": "https://oncall-endpoint.example.com",
},
"type": "oncall",
},
],
"name": "OnCall Conctact point",
},
{
"grafana_managed_receiver_configs": [
{
"metadata": {},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"name": "my new receiver",
"secureSettings": {},
"secureFields": {},
"settings": {
"addresses": "tester@grafana.com",
"singleEmail": false,
@ -141,8 +49,8 @@ exports[`alerting API server disabled should be able to test and save a receiver
"type": "email",
},
],
"name": "my new receiver",
"title": "my new receiver",
},
],
},
]
`;

@ -5,7 +5,6 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { render, screen, waitFor } from 'test/test-utils';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { config } from '@grafana/runtime';
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
import {
setOnCallFeatures,
@ -58,49 +57,40 @@ describe('GrafanaReceiverForm', () => {
]);
});
describe('alertingApiServer', () => {
beforeEach(() => {
config.featureToggles.alertingApiServer = true;
it('handles nested secure fields correctly', async () => {
const capturedRequests = captureRequests(
(req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST'
);
const { user } = renderWithProvider(<GrafanaReceiverForm />);
const { type, click } = user;
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
// Select MQTT receiver and fill out basic required fields for contact point
await clickSelectOption(await byTestId('items.0.type').find(), 'MQTT');
await type(screen.getByLabelText(/^name/i), 'mqtt contact point');
await type(screen.getByLabelText(/broker url/i), 'broker url');
await type(screen.getByLabelText(/topic/i), 'topic');
// Fill out fields that we know will be nested secure fields
await click(screen.getByText(/optional mqtt settings/i));
await click(screen.getByRole('button', { name: /^Add$/i }));
await type(screen.getByLabelText(/ca certificate/i), 'some cert');
await click(screen.getByRole('button', { name: /save contact point/i }));
const [request] = await capturedRequests;
const postRequestbody = await request.clone().json();
const integrationPayload = postRequestbody.spec.integrations[0];
expect(integrationPayload.settings.tlsConfig).toEqual({
// Expect the payload to have included the value of a secret field
caCertificate: 'some cert',
// And to not have removed other values (which would happen if we incorrectly merged settings together)
insecureSkipVerify: false,
});
afterEach(() => {
config.featureToggles.alertingApiServer = false;
});
it('handles nested secure fields correctly', async () => {
const capturedRequests = captureRequests(
(req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST'
);
const { user } = renderWithProvider(<GrafanaReceiverForm />);
const { type, click } = user;
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
// Select MQTT receiver and fill out basic required fields for contact point
await clickSelectOption(await byTestId('items.0.type').find(), 'MQTT');
await type(screen.getByLabelText(/^name/i), 'mqtt contact point');
await type(screen.getByLabelText(/broker url/i), 'broker url');
await type(screen.getByLabelText(/topic/i), 'topic');
// Fill out fields that we know will be nested secure fields
await click(screen.getByText(/optional mqtt settings/i));
await click(screen.getByRole('button', { name: /^Add$/i }));
await type(screen.getByLabelText(/ca certificate/i), 'some cert');
await click(screen.getByRole('button', { name: /save contact point/i }));
const [request] = await capturedRequests;
const postRequestbody = await request.clone().json();
const integrationPayload = postRequestbody.spec.integrations[0];
expect(integrationPayload.settings.tlsConfig).toEqual({
// Expect the payload to have included the value of a secret field
caCertificate: 'some cert',
// And to not have removed other values (which would happen if we incorrectly merged settings together)
insecureSkipVerify: false,
});
expect(postRequestbody).toMatchSnapshot();
});
expect(postRequestbody).toMatchSnapshot();
});
describe('OnCall contact point', () => {

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GrafanaReceiverForm alertingApiServer handles nested secure fields correctly 1`] = `
exports[`GrafanaReceiverForm handles nested secure fields correctly 1`] = `
{
"metadata": {},
"spec": {

@ -6,7 +6,6 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants';
@ -86,8 +85,6 @@ describe('getTemplateOptions function', () => {
});
describe('TemplatesPicker', () => {
testWithFeatureToggles(['alertingApiServer']);
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,

@ -136,18 +136,14 @@ describe('Can create a new grafana managed alert using simplified routing', () =
expect(serializedRequests).toMatchSnapshot();
});
describe('alertingApiServer enabled', () => {
testWithFeatureToggles(['alertingApiServer']);
it('allows selecting a contact point when using alerting API server', async () => {
const { user } = renderRuleEditor();
it('allows selecting a contact point', async () => {
const { user } = renderRuleEditor();
await user.click(await ui.inputs.simplifiedRouting.contactPointRouting.find());
await user.click(await ui.inputs.simplifiedRouting.contactPointRouting.find());
await selectContactPoint(user, 'Email');
await selectContactPoint(user, 'Email');
expect(await screen.findByText('Email')).toBeInTheDocument();
});
expect(await screen.findByText('Email')).toBeInTheDocument();
});
describe('switch modes enabled', () => {

@ -2,7 +2,6 @@ import { render, screen, waitFor, within } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { AccessControlAction } from 'app/types/accessControl';
import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types';
@ -114,13 +113,7 @@ const folder: Folder = {
title: 'title',
};
describe.each([
// k8s API enabled
true,
// k8s API disabled
false,
])('NotificationPreview with alertingApiServer=%p', (apiServerEnabled) => {
apiServerEnabled ? testWithFeatureToggles(['alertingApiServer']) : testWithFeatureToggles([]);
describe('NotificationPreview', () => {
jest.retryTimes(2);
it('should render notification preview without alert manager label, when having only one alert manager configured to receive alerts', async () => {

@ -254,3 +254,27 @@ export const makeAllK8sGetEndpointsFail = (
})
);
};
export const makeAllK8sEndpointsFail = (
uid: string,
message = 'could not find an Alertmanager configuration',
status = 500
) => {
server.use(
http.all(ALERTING_API_SERVER_BASE_URL + '/*', () => {
const errorResponse: ApiMachineryError = {
kind: 'Status',
apiVersion: 'v1',
metadata: {},
status: 'Failure',
details: {
uid,
},
message,
code: status,
reason: '',
};
return HttpResponse.json<ApiMachineryError>(errorResponse, { status });
})
);
};

@ -22,7 +22,7 @@ const resourceDescriptionsMap: Record<string, Description> = {
* */
const resourceDetailsMap: Record<string, Record<string, ResourcePermission[]>> = {
receivers: {
lotsaEmails: [
'lotsa-emails': [
{
id: 123,
roleName: 'somerole:name',

@ -1,12 +1,19 @@
import { camelCase } from 'lodash';
import { HttpResponse, http } from 'msw';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import {
getAlertmanagerConfig,
setAlertmanagerConfig,
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
const usedByPolicies = ['grafana-default-email'];
const usedByRules = ['grafana-default-email'];
const cannotBeEdited = ['grafana-default-email'];
const cannotBeDeleted = ['grafana-default-email'];
const getReceiversList = () => {
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
@ -20,12 +27,14 @@ const getReceiversList = () => {
return {
metadata: {
// This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses
uid: camelCase(contactPoint.name),
uid: contactPoint.name,
annotations: {
[K8sAnnotations.Provenance]: provenance,
[K8sAnnotations.AccessAdmin]: 'true',
[K8sAnnotations.AccessDelete]: 'true',
[K8sAnnotations.AccessWrite]: 'true',
[K8sAnnotations.AccessDelete]: cannotBeDeleted.includes(contactPoint.name) ? 'false' : 'true',
[K8sAnnotations.AccessWrite]: cannotBeEdited.includes(contactPoint.name) ? 'false' : 'true',
[K8sAnnotations.InUseRoutes]: usedByPolicies.includes(contactPoint.name) ? '1' : '0',
[K8sAnnotations.InUseRules]: usedByRules.includes(contactPoint.name) ? '1' : '0',
},
},
spec: {
@ -46,6 +55,35 @@ const listNamespacedReceiverHandler = () =>
return HttpResponse.json(getReceiversList());
});
const getNamespacedReceiverHandler = () =>
http.get<{ namespace: string; name: string }>(
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`,
({ params }) => {
const { name } = params;
const receivers = getReceiversList();
const matchedReceiver = receivers.items.find((receiver) => receiver.metadata.uid === name);
if (!matchedReceiver) {
return HttpResponse.json({}, { status: 404 });
}
return HttpResponse.json(matchedReceiver);
}
);
const updateNamespacedReceiverHandler = () =>
http.put<{ namespace: string; name: string }>(
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`,
async ({ params, request }) => {
// TODO: Make this update the internal config so API calls "persist"
const { name } = params;
const parsedReceivers = getReceiversList();
const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name);
if (!matchedReceiver) {
return HttpResponse.json({}, { status: 404 });
}
return HttpResponse.json(parsedReceivers);
}
);
const createNamespacedReceiverHandler = () =>
http.post<{ namespace: string }>(
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers`,
@ -60,17 +98,29 @@ const deleteNamespacedReceiverHandler = () =>
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`,
({ params }) => {
const { name } = params;
const parsedReceivers = getReceiversList();
const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name);
if (matchedReceiver) {
return HttpResponse.json(parsedReceivers);
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
const matchedReceiver = config.alertmanager_config?.receivers?.find((receiver) => receiver.name === name);
if (!matchedReceiver) {
return HttpResponse.json({}, { status: 404 });
}
return HttpResponse.json({}, { status: 404 });
const newConfig = config.alertmanager_config?.receivers?.filter((receiver) => receiver.name !== name);
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, {
...config,
alertmanager_config: {
...config.alertmanager_config,
receivers: newConfig,
},
});
const parsedReceivers = getReceiversList();
return HttpResponse.json(parsedReceivers);
}
);
const handlers = [
listNamespacedReceiverHandler(),
getNamespacedReceiverHandler(),
updateNamespacedReceiverHandler(),
createNamespacedReceiverHandler(),
deleteNamespacedReceiverHandler(),
];

@ -1,4 +1,3 @@
import { config } from '@grafana/runtime';
import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
@ -10,8 +9,7 @@ import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/u
* and the `alertingApiServer` feature toggle being enabled
*/
export const shouldUseK8sApi = (alertmanager?: string) => {
const featureToggleEnabled = config.featureToggles.alertingApiServer;
return featureToggleEnabled && alertmanager === GRAFANA_RULES_SOURCE_NAME;
return alertmanager === GRAFANA_RULES_SOURCE_NAME;
};
type EntityToCheck = {

Loading…
Cancel
Save