diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 9bde512b504..257d2e23a07 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -12,6 +12,11 @@ import ( "time" "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/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -43,11 +48,8 @@ import ( secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "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) { @@ -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), secretsService, nil, alertMetrics, mockFolder, fakeAccessControl, dashboardService, nil, bus, fakeAccessControlService, annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, - httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(t, err) diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 55d29355952..811edf6e893 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -78,6 +79,7 @@ type API struct { Historian Historian Tracer tracing.Tracer AppUrl *url.URL + UserService user.Service // Hooks can be used to replace API handlers for specific paths. Hooks *Hooks @@ -135,6 +137,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { amConfigStore: api.AlertingStore, amRefresher: api.MultiOrgAlertmanager, featureManager: api.FeatureManager, + userService: api.UserService, }, ), m) api.RegisterTestingApiEndpoints(NewTestingApi( diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 55783cfe853..f0cc114d77a 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "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/util" ) @@ -53,6 +54,7 @@ type RulerSrv struct { cfg *setting.UnifiedAlertingSettings conditionValidator ConditionValidator authz RuleAccessControlService + userService user.Service amConfigStore AMConfigStore amRefresher AMRefresher @@ -211,7 +213,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam result := apimodels.NamespaceConfigResponse{} 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) @@ -246,7 +248,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa result := apimodels.RuleGroupConfigResponse{ // nolint:staticcheck - GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords), + GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context())), } 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) 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) } @@ -323,7 +325,7 @@ func (srv RulerSrv) RouteGetRuleByUID(c *contextmodel.ReqContext, ruleUID string 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) } @@ -533,7 +535,7 @@ func changesToResponse(finalChanges *store.GroupDelta) response.Response { 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() ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(rules)) var interval time.Duration @@ -541,7 +543,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov interval = time.Duration(rules[0].IntervalSeconds) * time.Second } for _, r := range rules { - ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords)) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords, userIdToName)) } return apimodels.GettableRuleGroupConfig{ 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 if prov, exists := provenanceRecords[r.ResourceID()]; exists { provenance = prov @@ -564,6 +566,7 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[stri Condition: r.Condition, Data: ApiAlertQueriesFromAlertQueries(r.Data), Updated: r.Updated, + UpdatedBy: userIdToName(r.UpdatedBy), IntervalSeconds: r.IntervalSeconds, Version: r.Version, UID: r.UID, @@ -724,3 +727,28 @@ func (srv RulerSrv) searchAuthorizedAlertRules(ctx context.Context, q authorized } 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, + } + } +} diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index ede0a00b799..eec2d83729a 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -32,7 +32,9 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "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/util" "github.com/grafana/grafana/pkg/util/cmputil" "github.com/grafana/grafana/pkg/web" ) @@ -357,6 +359,72 @@ func TestRouteGetRuleByUID(t *testing.T) { require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection) 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) { @@ -659,6 +727,7 @@ func createService(store *fakes.RuleStore) *RulerSrv { amConfigStore: &fakeAMRefresher{}, amRefresher: &fakeAMRefresher{}, featureManager: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules), + userService: usertest.NewUserServiceFake(), } } diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index a6383efa0b6..502e167f10c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -364,7 +364,7 @@ const ( type PostableExtendedRuleNode struct { // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) *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"` } @@ -401,7 +401,7 @@ func (n *PostableExtendedRuleNode) validate() error { type GettableExtendedRuleNode struct { // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) *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"` } @@ -541,6 +541,7 @@ type GettableGrafanaRule struct { Condition string `json:"condition" yaml:"condition"` Data []AlertQuery `json:"data" yaml:"data"` Updated time.Time `json:"updated" yaml:"updated"` + UpdatedBy *UserInfo `json:"updated_by" yaml:"updated_by"` IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` Version int64 `json:"version" yaml:"version"` UID string `json:"uid" yaml:"uid"` @@ -555,6 +556,12 @@ type GettableGrafanaRule struct { 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. type AlertQuery struct { // RefID is the unique identifier of the query, set by the frontend call. diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 51ad5caffe7..2270620dc76 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -49,6 +49,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -78,6 +79,7 @@ func ProvideService( ruleStore *store.DBstore, httpClientProvider httpclient.Provider, resourcePermissions accesscontrol.ReceiverPermissionsService, + userService user.Service, ) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, @@ -106,6 +108,7 @@ func ProvideService( store: ruleStore, httpClientProvider: httpClientProvider, ResourcePermissions: resourcePermissions, + userService: userService, } if ng.IsDisabled() { @@ -154,6 +157,7 @@ type AlertNG struct { ResourcePermissions accesscontrol.ReceiverPermissionsService annotationsRepo annotations.Repository store *store.DBstore + userService user.Service bus bus.Bus pluginsStore pluginstore.Store @@ -496,6 +500,7 @@ func (ng *AlertNG) init() error { Historian: history, Hooks: api.NewHooks(ng.Log), Tracer: ng.tracer, + UserService: ng.userService, } ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 8ea4fef13c3..0d356fabd7b 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -37,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "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/util" ) @@ -90,7 +91,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration, opts ...TestEnvOpti ng, err := ngalert.ProvideService( 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, - 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) diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 4030fe0c19b..5cf4a3acabf 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -50,6 +50,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "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/util" ) @@ -512,7 +513,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe _, err = ngalert.ProvideService( 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{}, - 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) _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), cfg, quotaService, storesrv.ProvideSystemUsersService()) diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 5c101cb3768..568205ef36e 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "path" - "regexp" "slices" "strings" "testing" @@ -121,6 +120,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) { pathsToIgnore := []string{ "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy", "GrafanaManagedAlert.UID", "GrafanaManagedAlert.ID", "GrafanaManagedAlert.Data.Model", @@ -422,6 +422,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) { pathsToIgnore := []string{ "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy", "GrafanaManagedAlert.UID", "GrafanaManagedAlert.ID", "GrafanaManagedAlert.Data.Model", @@ -1144,6 +1145,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1183,6 +1188,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1234,6 +1243,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1498,8 +1511,9 @@ func TestIntegrationRuleCreate(t *testing.T) { client.CreateFolder(t, namespaceUID, namespaceUID) cases := []struct { - name string - config apimodels.PostableRuleGroupConfig + name string + config apimodels.PostableRuleGroupConfig + expected apimodels.GettableRuleGroupConfig }{{ name: "can create a rule with UTF-8", config: apimodels.PostableRuleGroupConfig{ @@ -1514,8 +1528,7 @@ func TestIntegrationRuleCreate(t *testing.T) { "_bar1": "baz🙂", }, Annotations: map[string]string{ - "Προμηθέας": "prom", // Prometheus in Greek - "犬": "Shiba Inu", // Dog in Japanese + "Προμηθέας": "prom", // Prometheus in Greek }, }, 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 { @@ -1545,6 +1604,27 @@ func TestIntegrationRuleCreate(t *testing.T) { require.Len(t, resp.Created, 1) require.Len(t, resp.Updated, 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) }) }) + 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) { @@ -2438,6 +2530,10 @@ func TestIntegrationQuota(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":2, @@ -2510,13 +2606,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) - - re := regexp.MustCompile(`"uid":"([\w|-]+)"`) - 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(`{ + body, _ := rulesNamespaceWithoutVariableValues(t, b) + expectedGetRulesResponseBody := `{ "default": [ { "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, "is_paused": false, "version": 1, - "uid": "", - "namespace_uid": %q, + "uid": "uid", + "namespace_uid": "nsuid", "rule_group": "arulegroup", "no_data_state": "NoData", "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) { 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_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":1, @@ -3075,6 +3174,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":1, @@ -3389,6 +3492,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":2, @@ -3504,6 +3611,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused":false, "version":3, @@ -3598,6 +3709,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused":false, "version":3, @@ -4289,6 +4404,7 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st rule.GrafanaManagedAlert.UID = "uid" rule.GrafanaManagedAlert.NamespaceUID = "nsuid" rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC) + rule.GrafanaManagedAlert.UpdatedBy.UID = "uid" } } }