Alerting: Expose updated_by in rules GET APIs (#99525)

---------

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Yuri Tseretyan 2025-01-27 14:31:40 -05:00 committed by GitHub
parent 32ae292334
commit d71904cb27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 265 additions and 33 deletions

View File

@ -12,6 +12,11 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
@ -43,11 +48,8 @@ import (
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
) )
func Test_NoopServiceDoesNothing(t *testing.T) { func Test_NoopServiceDoesNothing(t *testing.T) {
@ -874,7 +876,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf
cfg, featureToggles, nil, nil, rr, sqlStore, kvStore, nil, nil, quotatest.New(false, nil), cfg, featureToggles, nil, nil, rr, sqlStore, kvStore, nil, nil, quotatest.New(false, nil),
secretsService, nil, alertMetrics, mockFolder, fakeAccessControl, dashboardService, nil, bus, fakeAccessControlService, secretsService, nil, alertMetrics, mockFolder, fakeAccessControl, dashboardService, nil, bus, fakeAccessControlService,
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore,
httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(),
) )
require.NoError(t, err) require.NoError(t, err)

View File

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -78,6 +79,7 @@ type API struct {
Historian Historian Historian Historian
Tracer tracing.Tracer Tracer tracing.Tracer
AppUrl *url.URL AppUrl *url.URL
UserService user.Service
// Hooks can be used to replace API handlers for specific paths. // Hooks can be used to replace API handlers for specific paths.
Hooks *Hooks Hooks *Hooks
@ -135,6 +137,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
amConfigStore: api.AlertingStore, amConfigStore: api.AlertingStore,
amRefresher: api.MultiOrgAlertmanager, amRefresher: api.MultiOrgAlertmanager,
featureManager: api.FeatureManager, featureManager: api.FeatureManager,
userService: api.UserService,
}, },
), m) ), m)
api.RegisterTestingApiEndpoints(NewTestingApi( api.RegisterTestingApiEndpoints(NewTestingApi(

View File

@ -27,6 +27,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -53,6 +54,7 @@ type RulerSrv struct {
cfg *setting.UnifiedAlertingSettings cfg *setting.UnifiedAlertingSettings
conditionValidator ConditionValidator conditionValidator ConditionValidator
authz RuleAccessControlService authz RuleAccessControlService
userService user.Service
amConfigStore AMConfigStore amConfigStore AMConfigStore
amRefresher AMRefresher amRefresher AMRefresher
@ -211,7 +213,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam
result := apimodels.NamespaceConfigResponse{} result := apimodels.NamespaceConfigResponse{}
for groupKey, rules := range ruleGroups { for groupKey, rules := range ruleGroups {
result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords)) result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context())))
} }
return response.JSON(http.StatusAccepted, result) return response.JSON(http.StatusAccepted, result)
@ -246,7 +248,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa
result := apimodels.RuleGroupConfigResponse{ result := apimodels.RuleGroupConfigResponse{
// nolint:staticcheck // nolint:staticcheck
GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords), GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context())),
} }
return response.JSON(http.StatusAccepted, result) return response.JSON(http.StatusAccepted, result)
} }
@ -300,7 +302,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res
srv.log.Error("Namespace not visible to the user", "user", id, "userNamespace", userNamespace, "namespace", groupKey.NamespaceUID) srv.log.Error("Namespace not visible to the user", "user", id, "userNamespace", userNamespace, "namespace", groupKey.NamespaceUID)
continue continue
} }
result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords)) result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context())))
} }
return response.JSON(http.StatusOK, result) return response.JSON(http.StatusOK, result)
} }
@ -323,7 +325,7 @@ func (srv RulerSrv) RouteGetRuleByUID(c *contextmodel.ReqContext, ruleUID string
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule provenance", err) return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule provenance", err)
} }
result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance}) result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance}, srv.resolveUserIdToNameFn(ctx))
return response.JSON(http.StatusOK, result) return response.JSON(http.StatusOK, result)
} }
@ -533,7 +535,7 @@ func changesToResponse(finalChanges *store.GroupDelta) response.Response {
return response.JSON(http.StatusAccepted, body) return response.JSON(http.StatusAccepted, body)
} }
func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableRuleGroupConfig { func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, provenanceRecords map[string]ngmodels.Provenance, userIdToName userIDToUserInfoFn) apimodels.GettableRuleGroupConfig {
rules.SortByGroupIndex() rules.SortByGroupIndex()
ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(rules)) ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(rules))
var interval time.Duration var interval time.Duration
@ -541,7 +543,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov
interval = time.Duration(rules[0].IntervalSeconds) * time.Second interval = time.Duration(rules[0].IntervalSeconds) * time.Second
} }
for _, r := range rules { for _, r := range rules {
ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords)) ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords, userIdToName))
} }
return apimodels.GettableRuleGroupConfig{ return apimodels.GettableRuleGroupConfig{
Name: groupName, Name: groupName,
@ -550,7 +552,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov
} }
} }
func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableExtendedRuleNode { func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, userIdToName userIDToUserInfoFn) apimodels.GettableExtendedRuleNode {
provenance := ngmodels.ProvenanceNone provenance := ngmodels.ProvenanceNone
if prov, exists := provenanceRecords[r.ResourceID()]; exists { if prov, exists := provenanceRecords[r.ResourceID()]; exists {
provenance = prov provenance = prov
@ -564,6 +566,7 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[stri
Condition: r.Condition, Condition: r.Condition,
Data: ApiAlertQueriesFromAlertQueries(r.Data), Data: ApiAlertQueriesFromAlertQueries(r.Data),
Updated: r.Updated, Updated: r.Updated,
UpdatedBy: userIdToName(r.UpdatedBy),
IntervalSeconds: r.IntervalSeconds, IntervalSeconds: r.IntervalSeconds,
Version: r.Version, Version: r.Version,
UID: r.UID, UID: r.UID,
@ -724,3 +727,28 @@ func (srv RulerSrv) searchAuthorizedAlertRules(ctx context.Context, q authorized
} }
return byGroupKey, totalGroups, nil return byGroupKey, totalGroups, nil
} }
type userIDToUserInfoFn func(id *ngmodels.UserUID) *apimodels.UserInfo
// getIdentityName returns name of either user or service account
func (srv RulerSrv) resolveUserIdToNameFn(ctx context.Context) userIDToUserInfoFn {
return func(id *ngmodels.UserUID) *apimodels.UserInfo {
if id == nil {
return nil
}
u, err := srv.userService.GetByUID(ctx, &user.GetUserByUIDQuery{
UID: string(*id),
})
var result string
if err != nil {
srv.log.FromContext(ctx).Warn("Failed to get user by uid. Defaulting to an empty name", "uid", id, "error", err)
}
if u != nil {
result = u.NameOrFallback()
}
return &apimodels.UserInfo{
UID: string(*id),
Name: result,
}
}
}

View File

@ -32,7 +32,9 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/cmputil" "github.com/grafana/grafana/pkg/util/cmputil"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -357,6 +359,72 @@ func TestRouteGetRuleByUID(t *testing.T) {
require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title) require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title)
require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection)
require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedNotificationsSection) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedNotificationsSection)
t.Run("should resolve Updated_by with user service", func(t *testing.T) {
testcases := []struct {
desc string
UpdatedBy *models.UserUID
User *user.User
UserServiceError error
Expected *apimodels.UserInfo
}{
{
desc: "nil if UpdatedBy is nil",
UpdatedBy: nil,
User: nil,
UserServiceError: nil,
Expected: nil,
},
{
desc: "just UID if user is not found",
UpdatedBy: util.Pointer(models.UserUID("test-uid")),
User: nil,
UserServiceError: nil,
Expected: &apimodels.UserInfo{
UID: "test-uid",
},
},
{
desc: "just UID if error",
UpdatedBy: util.Pointer(models.UserUID("test-uid")),
UserServiceError: errors.New("error"),
Expected: &apimodels.UserInfo{
UID: "test-uid",
},
},
{
desc: "login if it's known user",
UpdatedBy: util.Pointer(models.UserUID("test-uid")),
User: &user.User{
Login: "Test",
},
UserServiceError: nil,
Expected: &apimodels.UserInfo{
UID: "test-uid",
Name: "Test",
},
},
}
for _, tc := range testcases {
t.Run(tc.desc, func(t *testing.T) {
expectedRule.UpdatedBy = tc.UpdatedBy
svc := createService(ruleStore)
usvc := usertest.NewUserServiceFake()
usvc.ExpectedUser = tc.User
usvc.ExpectedError = tc.UserServiceError
svc.userService = usvc
response := svc.RouteGetRuleByUID(req, expectedRule.UID)
require.Equal(t, http.StatusOK, response.Status())
result := &apimodels.GettableExtendedRuleNode{}
require.NoError(t, json.Unmarshal(response.Body(), result))
require.NotNil(t, result)
require.Equal(t, tc.Expected, result.GrafanaManagedAlert.UpdatedBy)
})
}
})
}) })
t.Run("error when fetching rule with non-existent UID", func(t *testing.T) { t.Run("error when fetching rule with non-existent UID", func(t *testing.T) {
@ -659,6 +727,7 @@ func createService(store *fakes.RuleStore) *RulerSrv {
amConfigStore: &fakeAMRefresher{}, amConfigStore: &fakeAMRefresher{},
amRefresher: &fakeAMRefresher{}, amRefresher: &fakeAMRefresher{},
featureManager: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules), featureManager: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules),
userService: usertest.NewUserServiceFake(),
} }
} }

View File

@ -364,7 +364,7 @@ const (
type PostableExtendedRuleNode struct { type PostableExtendedRuleNode struct {
// note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2)
*ApiRuleNode `yaml:",inline"` *ApiRuleNode `yaml:",inline"`
//GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` // GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"`
GrafanaManagedAlert *PostableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"` GrafanaManagedAlert *PostableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"`
} }
@ -401,7 +401,7 @@ func (n *PostableExtendedRuleNode) validate() error {
type GettableExtendedRuleNode struct { type GettableExtendedRuleNode struct {
// note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2)
*ApiRuleNode `yaml:",inline"` *ApiRuleNode `yaml:",inline"`
//GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` // GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"`
GrafanaManagedAlert *GettableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"` GrafanaManagedAlert *GettableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"`
} }
@ -541,6 +541,7 @@ type GettableGrafanaRule struct {
Condition string `json:"condition" yaml:"condition"` Condition string `json:"condition" yaml:"condition"`
Data []AlertQuery `json:"data" yaml:"data"` Data []AlertQuery `json:"data" yaml:"data"`
Updated time.Time `json:"updated" yaml:"updated"` Updated time.Time `json:"updated" yaml:"updated"`
UpdatedBy *UserInfo `json:"updated_by" yaml:"updated_by"`
IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"`
Version int64 `json:"version" yaml:"version"` Version int64 `json:"version" yaml:"version"`
UID string `json:"uid" yaml:"uid"` UID string `json:"uid" yaml:"uid"`
@ -555,6 +556,12 @@ type GettableGrafanaRule struct {
Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"`
} }
// UserInfo represents user-related information, including a unique identifier and a name.
type UserInfo struct {
UID string `json:"uid"`
Name string `json:"name"`
}
// AlertQuery represents a single query associated with an alert definition. // AlertQuery represents a single query associated with an alert definition.
type AlertQuery struct { type AlertQuery struct {
// RefID is the unique identifier of the query, set by the frontend call. // RefID is the unique identifier of the query, set by the frontend call.

View File

@ -49,6 +49,7 @@ import (
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -78,6 +79,7 @@ func ProvideService(
ruleStore *store.DBstore, ruleStore *store.DBstore,
httpClientProvider httpclient.Provider, httpClientProvider httpclient.Provider,
resourcePermissions accesscontrol.ReceiverPermissionsService, resourcePermissions accesscontrol.ReceiverPermissionsService,
userService user.Service,
) (*AlertNG, error) { ) (*AlertNG, error) {
ng := &AlertNG{ ng := &AlertNG{
Cfg: cfg, Cfg: cfg,
@ -106,6 +108,7 @@ func ProvideService(
store: ruleStore, store: ruleStore,
httpClientProvider: httpClientProvider, httpClientProvider: httpClientProvider,
ResourcePermissions: resourcePermissions, ResourcePermissions: resourcePermissions,
userService: userService,
} }
if ng.IsDisabled() { if ng.IsDisabled() {
@ -154,6 +157,7 @@ type AlertNG struct {
ResourcePermissions accesscontrol.ReceiverPermissionsService ResourcePermissions accesscontrol.ReceiverPermissionsService
annotationsRepo annotations.Repository annotationsRepo annotations.Repository
store *store.DBstore store *store.DBstore
userService user.Service
bus bus.Bus bus bus.Bus
pluginsStore pluginstore.Store pluginsStore pluginstore.Store
@ -496,6 +500,7 @@ func (ng *AlertNG) init() error {
Historian: history, Historian: history,
Hooks: api.NewHooks(ng.Log), Hooks: api.NewHooks(ng.Log),
Tracer: ng.tracer, Tracer: ng.tracer,
UserService: ng.userService,
} }
ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())

View File

@ -37,6 +37,7 @@ import (
"github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -90,7 +91,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration, opts ...TestEnvOpti
ng, err := ngalert.ProvideService( ng, err := ngalert.ProvideService(
cfg, options.featureToggles, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil), cfg, options.featureToggles, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil),
secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac,
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(),
) )
require.NoError(tb, err) require.NoError(tb, err)

View File

@ -50,6 +50,7 @@ import (
"github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -512,7 +513,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
_, err = ngalert.ProvideService( _, err = ngalert.ProvideService(
cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, ngalertfakes.NewFakeKVStore(t), nil, nil, quotaService, cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, ngalertfakes.NewFakeKVStore(t), nil, nil, quotaService,
secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{},
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(),
) )
require.NoError(t, err) require.NoError(t, err)
_, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), cfg, quotaService, storesrv.ProvideSystemUsersService()) _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), cfg, quotaService, storesrv.ProvideSystemUsersService())

View File

@ -9,7 +9,6 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"path" "path"
"regexp"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@ -121,6 +120,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
pathsToIgnore := []string{ pathsToIgnore := []string{
"GrafanaManagedAlert.Updated", "GrafanaManagedAlert.Updated",
"GrafanaManagedAlert.UpdatedBy",
"GrafanaManagedAlert.UID", "GrafanaManagedAlert.UID",
"GrafanaManagedAlert.ID", "GrafanaManagedAlert.ID",
"GrafanaManagedAlert.Data.Model", "GrafanaManagedAlert.Data.Model",
@ -422,6 +422,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
pathsToIgnore := []string{ pathsToIgnore := []string{
"GrafanaManagedAlert.Updated", "GrafanaManagedAlert.Updated",
"GrafanaManagedAlert.UpdatedBy",
"GrafanaManagedAlert.UID", "GrafanaManagedAlert.UID",
"GrafanaManagedAlert.ID", "GrafanaManagedAlert.ID",
"GrafanaManagedAlert.Data.Model", "GrafanaManagedAlert.Data.Model",
@ -1144,6 +1145,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
} }
}], }],
"updated": "2021-02-21T01:10:30Z", "updated": "2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds": 60, "intervalSeconds": 60,
"is_paused": false, "is_paused": false,
"version": 1, "version": 1,
@ -1183,6 +1188,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
} }
}], }],
"updated": "2021-02-21T01:10:30Z", "updated": "2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds": 60, "intervalSeconds": 60,
"is_paused": false, "is_paused": false,
"version": 1, "version": 1,
@ -1234,6 +1243,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
} }
}], }],
"updated": "2021-02-21T01:10:30Z", "updated": "2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds": 60, "intervalSeconds": 60,
"is_paused": false, "is_paused": false,
"version": 1, "version": 1,
@ -1498,8 +1511,9 @@ func TestIntegrationRuleCreate(t *testing.T) {
client.CreateFolder(t, namespaceUID, namespaceUID) client.CreateFolder(t, namespaceUID, namespaceUID)
cases := []struct { cases := []struct {
name string name string
config apimodels.PostableRuleGroupConfig config apimodels.PostableRuleGroupConfig
expected apimodels.GettableRuleGroupConfig
}{{ }{{
name: "can create a rule with UTF-8", name: "can create a rule with UTF-8",
config: apimodels.PostableRuleGroupConfig{ config: apimodels.PostableRuleGroupConfig{
@ -1514,8 +1528,7 @@ func TestIntegrationRuleCreate(t *testing.T) {
"_bar1": "baz🙂", "_bar1": "baz🙂",
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"Προμηθέας": "prom", // Prometheus in Greek "Προμηθέας": "prom", // Prometheus in Greek
"犬": "Shiba Inu", // Dog in Japanese
}, },
}, },
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
@ -1536,6 +1549,52 @@ func TestIntegrationRuleCreate(t *testing.T) {
}, },
}, },
}, },
expected: apimodels.GettableRuleGroupConfig{
Name: "test1",
Interval: model.Duration(time.Minute),
Rules: []apimodels.GettableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: util.Pointer(model.Duration(2 * time.Minute)),
Labels: map[string]string{
"foo🙂": "bar",
"_bar1": "baz🙂",
},
Annotations: map[string]string{
"Προμηθέας": "prom", // Prometheus in Greek
},
},
GrafanaManagedAlert: &apimodels.GettableGrafanaRule{
OrgID: 1,
Title: "test1 rule1",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(0),
To: apimodels.Duration(15 * time.Minute),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"}`),
},
},
UpdatedBy: &apimodels.UserInfo{
Name: "admin",
},
IntervalSeconds: 60,
Version: 1,
NamespaceUID: namespaceUID,
RuleGroup: "test1",
NoDataState: "NoData",
ExecErrState: "Alerting",
Provenance: "",
IsPaused: false,
Metadata: &apimodels.AlertRuleMetadata{},
},
},
},
},
}} }}
for _, tc := range cases { for _, tc := range cases {
@ -1545,6 +1604,27 @@ func TestIntegrationRuleCreate(t *testing.T) {
require.Len(t, resp.Created, 1) require.Len(t, resp.Created, 1)
require.Len(t, resp.Updated, 0) require.Len(t, resp.Updated, 0)
require.Len(t, resp.Deleted, 0) require.Len(t, resp.Deleted, 0)
got, _, _ := client.GetRulesGroupWithStatus(t, namespaceUID, tc.config.Name)
pathsToIgnore := []string{
"GrafanaManagedAlert.Updated",
"GrafanaManagedAlert.UpdatedBy.UID",
"GrafanaManagedAlert.UID",
"GrafanaManagedAlert.ID",
"GrafanaManagedAlert.NamespaceID",
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(tc.expected, got.GettableRuleGroupConfig, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.HasSuffix(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
require.Empty(t, diff)
}) })
} }
} }
@ -1696,6 +1776,18 @@ func TestIntegrationRuleUpdate(t *testing.T) {
require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body) require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body)
}) })
}) })
t.Run("should set updated_by", func(t *testing.T) {
group := generateAlertRuleGroup(1, alertRuleGen())
expected := model.Duration(10 * time.Second)
group.Rules[0].ApiRuleNode.For = &expected
_, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
require.NotNil(t, getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy)
assert.NotEmpty(t, getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy.UID)
assert.Equal(t, "grafana", getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy.Name)
})
} }
func TestIntegrationAlertAndGroupsQuery(t *testing.T) { func TestIntegrationAlertAndGroupsQuery(t *testing.T) {
@ -2438,6 +2530,10 @@ func TestIntegrationQuota(t *testing.T) {
} }
], ],
"updated":"2021-02-21T01:10:30Z", "updated":"2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds":60, "intervalSeconds":60,
"is_paused": false, "is_paused": false,
"version":2, "version":2,
@ -2510,13 +2606,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
body, _ := rulesNamespaceWithoutVariableValues(t, b)
re := regexp.MustCompile(`"uid":"([\w|-]+)"`) expectedGetRulesResponseBody := `{
b = re.ReplaceAll(b, []byte(`"uid":""`))
re = regexp.MustCompile(`"updated":"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"`)
b = re.ReplaceAll(b, []byte(`"updated":"2021-05-19T19:47:55Z"`))
expectedGetRulesResponseBody := fmt.Sprintf(`{
"default": [ "default": [
{ {
"name": "arulegroup", "name": "arulegroup",
@ -2553,12 +2644,16 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) {
} }
} }
], ],
"updated": "2021-05-19T19:47:55Z", "updated": "2021-02-21T01:10:30Z",
"updated_by" : {
"uid": "uid",
"name": "editor"
},
"intervalSeconds": 60, "intervalSeconds": 60,
"is_paused": false, "is_paused": false,
"version": 1, "version": 1,
"uid": "", "uid": "uid",
"namespace_uid": %q, "namespace_uid": "nsuid",
"rule_group": "arulegroup", "rule_group": "arulegroup",
"no_data_state": "NoData", "no_data_state": "NoData",
"exec_err_state": "Alerting", "exec_err_state": "Alerting",
@ -2573,8 +2668,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) {
] ]
} }
] ]
}`, namespaceUID) }`
assert.JSONEq(t, expectedGetRulesResponseBody, string(b)) assert.JSONEq(t, expectedGetRulesResponseBody, body)
}) })
t.Run("editor can not delete the folder because it contains Grafana 8 alerts", func(t *testing.T) { t.Run("editor can not delete the folder because it contains Grafana 8 alerts", func(t *testing.T) {
u := fmt.Sprintf("http://editor:editor@%s/api/folders/%s", grafanaListedAddr, namespaceUID) u := fmt.Sprintf("http://editor:editor@%s/api/folders/%s", grafanaListedAddr, namespaceUID)
@ -3033,6 +3128,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
} }
], ],
"updated":"2021-02-21T01:10:30Z", "updated":"2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds":60, "intervalSeconds":60,
"is_paused": false, "is_paused": false,
"version":1, "version":1,
@ -3075,6 +3174,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
} }
], ],
"updated":"2021-02-21T01:10:30Z", "updated":"2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds":60, "intervalSeconds":60,
"is_paused": false, "is_paused": false,
"version":1, "version":1,
@ -3389,6 +3492,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
} }
], ],
"updated":"2021-02-21T01:10:30Z", "updated":"2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds":60, "intervalSeconds":60,
"is_paused": false, "is_paused": false,
"version":2, "version":2,
@ -3504,6 +3611,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
} }
], ],
"updated":"2021-02-21T01:10:30Z", "updated":"2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds":60, "intervalSeconds":60,
"is_paused":false, "is_paused":false,
"version":3, "version":3,
@ -3598,6 +3709,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
} }
], ],
"updated":"2021-02-21T01:10:30Z", "updated":"2021-02-21T01:10:30Z",
"updated_by": {
"uid": "uid",
"name": "grafana"
},
"intervalSeconds":60, "intervalSeconds":60,
"is_paused":false, "is_paused":false,
"version":3, "version":3,
@ -4289,6 +4404,7 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st
rule.GrafanaManagedAlert.UID = "uid" rule.GrafanaManagedAlert.UID = "uid"
rule.GrafanaManagedAlert.NamespaceUID = "nsuid" rule.GrafanaManagedAlert.NamespaceUID = "nsuid"
rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC) rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC)
rule.GrafanaManagedAlert.UpdatedBy.UID = "uid"
} }
} }
} }