Alerting: Update Time Interval service to support renaming of resources (#91856)
* add RenameTimeIntervalInNotificationSettings to storage * update dependencies when the time interval is renamed --------- Co-authored-by: William Wernert <william.wernert@grafana.com>
This commit is contained in:
co-authored by
William Wernert
parent
3c5d799c8c
commit
135f6571a9
@@ -780,7 +780,7 @@ func TestMuteTimings(t *testing.T) {
|
||||
},
|
||||
MuteTimeIntervals: []string{anotherMuteTiming.Name},
|
||||
})
|
||||
status, response = apiClient.UpdateRouteWithStatus(t, route)
|
||||
status, response = apiClient.UpdateRouteWithStatus(t, route, false)
|
||||
requireStatusCode(t, http.StatusAccepted, status, response)
|
||||
|
||||
status, response = apiClient.DeleteMuteTimingWithStatus(t, anotherMuteTiming.Name)
|
||||
|
||||
@@ -783,7 +783,15 @@ func (a apiClient) GetRouteWithStatus(t *testing.T) (apimodels.Route, int, strin
|
||||
return sendRequest[apimodels.Route](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route) (int, string) {
|
||||
func (a apiClient) GetRoute(t *testing.T) apimodels.Route {
|
||||
t.Helper()
|
||||
|
||||
route, status, data := a.GetRouteWithStatus(t)
|
||||
requireStatusCode(t, http.StatusOK, status, data)
|
||||
return route
|
||||
}
|
||||
|
||||
func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route, noProvenance bool) (int, string) {
|
||||
t.Helper()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
@@ -793,6 +801,9 @@ func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route) (i
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/provisioning/policies", a.url), &buf)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if noProvenance {
|
||||
req.Header.Add("X-Disable-Provenance", "true")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
client := &http.Client{}
|
||||
@@ -807,6 +818,12 @@ func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route) (i
|
||||
return resp.StatusCode, string(body)
|
||||
}
|
||||
|
||||
func (a apiClient) UpdateRoute(t *testing.T, route apimodels.Route, noProvenance bool) {
|
||||
t.Helper()
|
||||
status, data := a.UpdateRouteWithStatus(t, route, noProvenance)
|
||||
requireStatusCode(t, http.StatusAccepted, status, data)
|
||||
}
|
||||
|
||||
func (a apiClient) GetRuleHistoryWithStatus(t *testing.T, ruleUID string) (data.Frame, int, string) {
|
||||
t.Helper()
|
||||
u, err := url.Parse(fmt.Sprintf("%s/api/v1/rules/history", a.url))
|
||||
@@ -877,6 +894,7 @@ func (a apiClient) GetActiveAlertsWithStatus(t *testing.T) (apimodels.AlertGroup
|
||||
}
|
||||
|
||||
func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
|
||||
t.Helper()
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
@@ -898,5 +916,6 @@ func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int)
|
||||
}
|
||||
|
||||
func requireStatusCode(t *testing.T, expected, actual int, response string) {
|
||||
t.Helper()
|
||||
require.Equalf(t, expected, actual, "Unexpected status. Response: %s", response)
|
||||
}
|
||||
|
||||
+10
-1
@@ -14,7 +14,7 @@
|
||||
]
|
||||
],
|
||||
"mute_time_intervals": [
|
||||
"test-interval"
|
||||
"test-interval", "persisted-interval"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -28,6 +28,15 @@
|
||||
"end_time": "23:59"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "persisted-interval",
|
||||
"time_intervals": [
|
||||
{
|
||||
"start_time": "06:00",
|
||||
"end_time": "23:59"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"notification_settings": {
|
||||
"receiver": "grafana-default-email",
|
||||
"mute_time_intervals": [
|
||||
"test-interval"
|
||||
"test-interval",
|
||||
"persisted-interval"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
@@ -25,6 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/tests/api/alerting"
|
||||
@@ -95,14 +98,21 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
existingInterval = actual
|
||||
})
|
||||
|
||||
t.Run("update should fail if name in the specification changes", func(t *testing.T) {
|
||||
t.Run("update should rename interval if name in the specification changes", func(t *testing.T) {
|
||||
if existingInterval == nil {
|
||||
t.Skip()
|
||||
}
|
||||
updated := existingInterval.DeepCopy()
|
||||
updated.Spec.Name = "another-newInterval"
|
||||
_, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
|
||||
actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, updated.Spec, actual.Spec)
|
||||
require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
|
||||
require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")
|
||||
|
||||
resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, actual, resource)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -641,6 +651,11 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient())
|
||||
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac)
|
||||
require.NoError(t, err)
|
||||
orgID := helper.Org1.Admin.Identity.GetOrgID()
|
||||
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
@@ -664,27 +679,100 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup)
|
||||
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
|
||||
|
||||
currentRoute := legacyCli.GetRoute(t)
|
||||
currentRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
|
||||
|
||||
adminK8sClient, err := versioned.NewForConfig(cliCfg)
|
||||
require.NoError(t, err)
|
||||
adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default")
|
||||
|
||||
intervals, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, intervals.Items, 1)
|
||||
require.Len(t, intervals.Items, 2)
|
||||
intervalIdx := slices.IndexFunc(intervals.Items, func(interval v0alpha1.TimeInterval) bool {
|
||||
return interval.Spec.Name == "test-interval"
|
||||
})
|
||||
interval := intervals.Items[intervalIdx]
|
||||
|
||||
intervalToDelete := intervals.Items[0]
|
||||
replace := func(input []string, oldName, newName string) []string {
|
||||
result := make([]string, 0, len(input))
|
||||
for _, s := range input {
|
||||
if s == oldName {
|
||||
result = append(result, newName)
|
||||
continue
|
||||
}
|
||||
result = append(result, s)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
t.Run("should fail to delete if time interval is used in rule and routes", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, intervalToDelete.Name, v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("should rename all references if name changes", func(t *testing.T) {
|
||||
renamed := interval.DeepCopy()
|
||||
renamed.Spec.Name += "-new"
|
||||
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
|
||||
for idx, rule := range updatedRuleGroup.Rules {
|
||||
expectedTimeIntervals := currentRuleGroup.Rules[idx].GrafanaManagedAlert.NotificationSettings.MuteTimeIntervals
|
||||
expectedTimeIntervals = replace(expectedTimeIntervals, interval.Spec.Name, actual.Spec.Name)
|
||||
assert.Equalf(t, expectedTimeIntervals, rule.GrafanaManagedAlert.NotificationSettings.MuteTimeIntervals, "time interval in rules should have been renamed but it did not")
|
||||
}
|
||||
|
||||
updatedRoute := legacyCli.GetRoute(t)
|
||||
for idx, route := range updatedRoute.Routes {
|
||||
expectedTimeIntervals := replace(currentRoute.Routes[idx].MuteTimeIntervals, interval.Spec.Name, actual.Spec.Name)
|
||||
assert.Equalf(t, expectedTimeIntervals, route.MuteTimeIntervals, "time interval in routes should have been renamed but it did not")
|
||||
}
|
||||
|
||||
interval = *actual
|
||||
})
|
||||
|
||||
t.Run("should fail if at least one resource is provisioned", func(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
renamed := interval.DeepCopy()
|
||||
renamed.Spec.Name += util.GenerateShortUID()
|
||||
|
||||
t.Run("provisioned route", func(t *testing.T) {
|
||||
require.NoError(t, db.SetProvenance(ctx, ¤tRoute, orgID, "API"))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, db.DeleteProvenance(ctx, ¤tRoute, orgID))
|
||||
})
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
t.Run("provisioned rules", func(t *testing.T) {
|
||||
ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
|
||||
resource := &ngmodels.AlertRule{UID: ruleUid}
|
||||
require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
|
||||
})
|
||||
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("should fail to delete if time interval is used in only rule", func(t *testing.T) {
|
||||
amConfig.AlertmanagerConfig.Route.Routes[0].MuteTimeIntervals = nil
|
||||
success, err := legacyCli.PostConfiguration(t, amConfig)
|
||||
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("should fail to delete if time interval is used in rule and routes", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, interval.Name, v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
err = adminClient.Delete(ctx, intervalToDelete.Name, v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
t.Run("should fail to delete if time interval is used in only rule", func(t *testing.T) {
|
||||
route := legacyCli.GetRoute(t)
|
||||
route.Routes[0].MuteTimeIntervals = nil
|
||||
legacyCli.UpdateRoute(t, route, true)
|
||||
|
||||
err = adminClient.Delete(ctx, interval.Name, v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user