Alerting: Fix k8s api route fingerprint to include ObjectMatchers and Provenance (#96591)

Fix route fingerprint to include ObjectMatchers and Provenance
This commit is contained in:
Matthew Jacobson 2024-11-16 08:38:30 -05:00 committed by GitHub
parent 9223cd8f59
commit 65097d4b54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 126 additions and 5 deletions

View File

@ -259,6 +259,9 @@ func writeToHash(sum hash.Hash, r *definitions.Route) {
for _, matcher := range r.Matchers {
writeString(matcher.String())
}
for _, matcher := range r.ObjectMatchers {
writeString(matcher.String())
}
for _, timeInterval := range r.MuteTimeIntervals {
writeString(timeInterval)
}
@ -269,6 +272,7 @@ func writeToHash(sum hash.Hash, r *definitions.Route) {
writeDuration(r.GroupWait)
writeDuration(r.GroupInterval)
writeDuration(r.RepeatInterval)
writeString(string(r.Provenance))
for _, route := range r.Routes {
writeToHash(sum, route)
}

View File

@ -2,11 +2,16 @@ package provisioning
import (
"context"
"encoding/json"
"errors"
"reflect"
"testing"
"time"
"github.com/grafana/alerting/definition"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -22,13 +27,14 @@ import (
func TestGetPolicyTree(t *testing.T) {
orgID := int64(1)
rev := getDefaultConfigRevision()
expectedVersion := calculateRouteFingerprint(*rev.Config.AlertmanagerConfig.Route)
expectedRoute := *rev.Config.AlertmanagerConfig.Route
expectedRoute.Provenance = definitions.Provenance(models.ProvenanceAPI)
expectedVersion := calculateRouteFingerprint(expectedRoute)
sut, store, prov := createNotificationPolicyServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return &rev, nil
}
expectedProvenance := models.ProvenanceAPI
prov.GetProvenanceFunc = func(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) {
return models.ProvenanceAPI, nil
}
@ -36,11 +42,9 @@ func TestGetPolicyTree(t *testing.T) {
tree, version, err := sut.GetPolicyTree(context.Background(), orgID)
require.NoError(t, err)
expectedRoute := *rev.Config.AlertmanagerConfig.Route
expectedRoute.Provenance = definitions.Provenance(models.ProvenanceAPI)
assert.Equal(t, expectedRoute, tree)
assert.Equal(t, expectedVersion, version)
assert.Equal(t, expectedProvenance, models.Provenance(tree.Provenance))
assert.Equal(t, expectedRoute.Provenance, tree.Provenance)
assert.Len(t, store.Calls, 1)
assert.Equal(t, "Get", store.Calls[0].Method)
@ -334,6 +338,119 @@ func TestResetPolicyTree(t *testing.T) {
})
}
func TestRoute_Fingerprint(t *testing.T) {
// Test that the fingerprint is stable.
mustRegex := func(rg string) config.Regexp {
var regex config.Regexp
require.NoError(t, json.Unmarshal([]byte(rg), &regex))
return regex
}
mustMatcher := func(t *testing.T, mt labels.MatchType, lbl, val string) *labels.Matcher {
m, err := labels.NewMatcher(mt, lbl, val)
require.NoError(t, err)
return m
}
baseRouteGen := func() definitions.Route {
return definitions.Route{
Receiver: "Receiver",
GroupByStr: []string{"GroupByStr1", "GroupByStr2"},
GroupBy: []model.LabelName{
"...",
},
GroupByAll: true,
Match: map[string]string{"Match1": "MatchValue1", "Match2": "MatchValue2"},
MatchRE: map[string]config.Regexp{
"MatchRE": mustRegex(`".*"`),
},
Matchers: config.Matchers{
mustMatcher(t, labels.MatchNotEqual, "Matchers1", "Matchers1Value"),
mustMatcher(t, labels.MatchEqual, "Matchers2", "Matchers2Value"),
mustMatcher(t, labels.MatchRegexp, "Matchers3", "Matchers3Value"),
},
ObjectMatchers: definitions.ObjectMatchers{
mustMatcher(t, labels.MatchNotRegexp, "ObjectMatchers1", "ObjectMatchers1Value"),
mustMatcher(t, labels.MatchRegexp, "ObjectMatchers2", "ObjectMatchers2Value"),
},
MuteTimeIntervals: []string{"MuteTimeIntervals1", "MuteTimeIntervals2"},
ActiveTimeIntervals: []string{"ActiveTimeIntervals1", "ActiveTimeIntervals2"},
Continue: true,
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
GroupInterval: util.Pointer(model.Duration(5 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(30 * time.Hour)),
Provenance: definitions.Provenance(models.ProvenanceAPI),
Routes: nil, // Nested routes are not included in the fingerprint test for simplicity.
}
}
completelyDifferentRoute := definitions.Route{
Receiver: "Receiver_2",
GroupByStr: []string{"GroupByStr1_2", "GroupByStr2_2"},
GroupBy: []model.LabelName{
"other",
},
GroupByAll: false,
Match: map[string]string{"Match1_2": "MatchValue1", "Match2": "MatchValue2_2"},
MatchRE: map[string]config.Regexp{
"MatchRE": mustRegex(`".+"`),
},
Matchers: config.Matchers{
mustMatcher(t, labels.MatchNotEqual, "Matchers1_2", "Matchers1Value"),
mustMatcher(t, labels.MatchEqual, "Matchers2", "Matchers2Value_2"),
mustMatcher(t, labels.MatchEqual, "Matchers3", "Matchers3Value"),
},
ObjectMatchers: definitions.ObjectMatchers{
mustMatcher(t, labels.MatchNotRegexp, "ObjectMatchers1_2", "ObjectMatchers1Value"),
mustMatcher(t, labels.MatchRegexp, "ObjectMatchers2", "ObjectMatchers2Value_2"),
},
MuteTimeIntervals: []string{"MuteTimeIntervals1_2", "MuteTimeIntervals2_2"},
ActiveTimeIntervals: []string{"ActiveTimeIntervals1_2", "ActiveTimeIntervals2_2"},
Continue: false,
GroupWait: util.Pointer(model.Duration(20 * time.Minute)),
GroupInterval: util.Pointer(model.Duration(50 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(300 * time.Hour)),
Provenance: definitions.Provenance(models.ProvenanceFile),
Routes: nil, // Nested routes are not included in the fingerprint test for simplicity, recursive fingerprinting is assumed.
}
t.Run("stable across code changes", func(t *testing.T) {
expectedFingerprint := "7faba12778df93b8" // If this is a valid fingerprint generation change, update the expected value.
assert.Equal(t, expectedFingerprint, calculateRouteFingerprint(baseRouteGen()))
})
t.Run("unstable across field modification", func(t *testing.T) {
fingerprint := calculateRouteFingerprint(baseRouteGen())
excludedFields := map[string]struct{}{
"Routes": {},
}
reflectVal := reflect.ValueOf(&completelyDifferentRoute).Elem()
receiverType := reflect.TypeOf((*definitions.Route)(nil)).Elem()
for i := 0; i < receiverType.NumField(); i++ {
field := receiverType.Field(i).Name
if _, ok := excludedFields[field]; ok {
continue
}
cp := baseRouteGen()
// Get the current field being modified.
v := reflect.ValueOf(&cp).Elem()
vf := v.Field(i)
otherField := reflectVal.Field(i)
if reflect.DeepEqual(otherField.Interface(), vf.Interface()) {
assert.Failf(t, "fields are identical", "Route field %s is the same as the original, test does not ensure instability across the field", field)
continue
}
// Set the field to the value of the completelyDifferentRoute.
vf.Set(otherField)
f2 := calculateRouteFingerprint(cp)
assert.NotEqualf(t, fingerprint, f2, "Route field %s does not seem to be used in fingerprint", field)
}
})
}
func createNotificationPolicyServiceSut() (*NotificationPolicyService, *legacy_storage.AlertmanagerConfigStoreFake, *fakes.FakeProvisioningStore) {
prov := fakes.NewFakeProvisioningStore()
configStore := &legacy_storage.AlertmanagerConfigStoreFake{