Alerting: Add admission hooks for rules app (#113429)
This adds validating admission hooks to enforce the requirements on AlertRules and RecordingRules that are currently enforced through the provisioning service and storage mechanisms in preparation of a consistent validation in both legacy storage and unified storage. It also adds a mutating admission hook to the app to ensure that folder annotations and folder labels are kept in sync so we can perform label-selector lists.
This commit is contained in:
@@ -461,7 +461,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.ErrorContains(t, err, "invalid alert rule")
|
||||
require.ErrorContains(t, err, "trigger interval must be a multiple of base evaluation interval")
|
||||
require.Nil(t, created)
|
||||
})
|
||||
}
|
||||
@@ -564,3 +564,148 @@ func TestIntegrationBasicAPI(t *testing.T) {
|
||||
t.Logf("Got error: %s", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationFolderLabelSyncAndValidation(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
client := common.NewAlertRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// Prepare two folders for label sync update scenario
|
||||
common.CreateTestFolder(t, helper, "test-folder-a")
|
||||
common.CreateTestFolder(t, helper, "test-folder-b")
|
||||
|
||||
baseGen := ngmodels.RuleGen.With(
|
||||
ngmodels.RuleMuts.WithUniqueUID(),
|
||||
ngmodels.RuleMuts.WithUniqueTitle(),
|
||||
ngmodels.RuleMuts.WithNamespaceUID("test-folder-a"),
|
||||
ngmodels.RuleMuts.WithGroupName("test-group"),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
)
|
||||
|
||||
t.Run("should keep folder label in sync with folder annotation on create and update", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
v0alpha1.FolderAnnotationKey: "test-folder-a",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.AlertRuleSpec{
|
||||
Title: rule.Title,
|
||||
Expressions: v0alpha1.AlertRuleExpressionMap{
|
||||
"A": {
|
||||
QueryType: util.Pointer(rule.Data[0].QueryType),
|
||||
DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{
|
||||
From: v0alpha1.AlertRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.AlertRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.AlertRuleIntervalTrigger{
|
||||
Interval: v0alpha1.AlertRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
created, err := client.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = client.Delete(ctx, created.Name, v1.DeleteOptions{}) }()
|
||||
|
||||
// On create, metadata.labels[v0alpha1.FolderLabelKey] should mirror annotation
|
||||
require.Equal(t, "test-folder-a", created.Labels[v0alpha1.FolderLabelKey])
|
||||
|
||||
// Update annotation to point to a different folder and ensure label follows
|
||||
updated := created.Copy().(*v0alpha1.AlertRule)
|
||||
if updated.Annotations == nil {
|
||||
updated.Annotations = map[string]string{}
|
||||
}
|
||||
updated.Annotations[v0alpha1.FolderAnnotationKey] = "test-folder-b"
|
||||
|
||||
after, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-folder-b", after.Annotations[v0alpha1.FolderAnnotationKey])
|
||||
require.Equal(t, "test-folder-b", after.Labels[v0alpha1.FolderLabelKey])
|
||||
})
|
||||
|
||||
t.Run("should fail to create rule without folder annotation", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{}, // missing grafana.app/folder
|
||||
},
|
||||
Spec: v0alpha1.AlertRuleSpec{
|
||||
Title: rule.Title,
|
||||
Expressions: v0alpha1.AlertRuleExpressionMap{
|
||||
"A": {
|
||||
QueryType: util.Pointer(rule.Data[0].QueryType),
|
||||
DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{
|
||||
From: v0alpha1.AlertRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.AlertRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.AlertRuleIntervalTrigger{
|
||||
Interval: v0alpha1.AlertRulePromDuration("10s"),
|
||||
},
|
||||
NoDataState: "NoData",
|
||||
ExecErrState: "Error",
|
||||
},
|
||||
}
|
||||
|
||||
created, err := client.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, created)
|
||||
})
|
||||
|
||||
t.Run("should fail to create rule with group labels preset", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
v0alpha1.FolderAnnotationKey: "test-folder-a",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
v0alpha1.GroupLabelKey: "some-group",
|
||||
v0alpha1.GroupIndexLabelKey: "0",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.AlertRuleSpec{
|
||||
Title: rule.Title,
|
||||
Expressions: v0alpha1.AlertRuleExpressionMap{
|
||||
"A": {
|
||||
QueryType: util.Pointer(rule.Data[0].QueryType),
|
||||
DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{
|
||||
From: v0alpha1.AlertRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.AlertRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.AlertRuleIntervalTrigger{Interval: v0alpha1.AlertRulePromDuration("10s")},
|
||||
NoDataState: "NoData",
|
||||
ExecErrState: "Error",
|
||||
},
|
||||
}
|
||||
|
||||
created, err := client.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, created)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.ErrorContains(t, err, "invalid alert rule")
|
||||
require.ErrorContains(t, err, "trigger interval must be a multiple of base evaluation interval")
|
||||
require.Nil(t, created)
|
||||
})
|
||||
}
|
||||
@@ -557,3 +557,139 @@ func TestIntegrationBasicAPI(t *testing.T) {
|
||||
t.Logf("Got error: %s", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationFolderLabelSyncAndValidation(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
client := common.NewRecordingRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// Prepare two folders for label sync update scenario
|
||||
common.CreateTestFolder(t, helper, "test-folder-a")
|
||||
common.CreateTestFolder(t, helper, "test-folder-b")
|
||||
|
||||
baseGen := ngmodels.RuleGen.With(
|
||||
ngmodels.RuleMuts.WithUniqueUID(),
|
||||
ngmodels.RuleMuts.WithUniqueTitle(),
|
||||
ngmodels.RuleMuts.WithNamespaceUID("test-folder-a"),
|
||||
ngmodels.RuleMuts.WithGroupName("test-group"),
|
||||
ngmodels.RuleMuts.WithAllRecordingRules(),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
)
|
||||
|
||||
t.Run("should keep folder label in sync with folder annotation on create and update", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
v0alpha1.FolderAnnotationKey: "test-folder-a",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Expressions: v0alpha1.RecordingRuleExpressionMap{
|
||||
"A": {
|
||||
QueryType: util.Pointer(rule.Data[0].QueryType),
|
||||
DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{Interval: v0alpha1.RecordingRulePromDuration("10s")},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := client.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = client.Delete(ctx, created.Name, v1.DeleteOptions{}) }()
|
||||
|
||||
// On create, metadata.labels[v0alpha1.FolderLabelKey] should mirror annotation
|
||||
require.Equal(t, "test-folder-a", created.Labels[v0alpha1.FolderLabelKey])
|
||||
|
||||
updated := created.Copy().(*v0alpha1.RecordingRule)
|
||||
if updated.Annotations == nil {
|
||||
updated.Annotations = map[string]string{}
|
||||
}
|
||||
updated.Annotations[v0alpha1.FolderAnnotationKey] = "test-folder-b"
|
||||
|
||||
after, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-folder-b", after.Annotations[v0alpha1.FolderAnnotationKey])
|
||||
require.Equal(t, "test-folder-b", after.Labels[v0alpha1.FolderLabelKey])
|
||||
})
|
||||
|
||||
t.Run("should fail to create recording rule without folder annotation", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Expressions: v0alpha1.RecordingRuleExpressionMap{
|
||||
"A": {
|
||||
QueryType: util.Pointer(rule.Data[0].QueryType),
|
||||
DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{Interval: v0alpha1.RecordingRulePromDuration("10s")},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := client.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, created)
|
||||
})
|
||||
|
||||
t.Run("should fail to create rule with group labels preset", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
v0alpha1.FolderAnnotationKey: "test-folder-a",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
v0alpha1.GroupLabelKey: "some-group",
|
||||
v0alpha1.GroupIndexLabelKey: "0",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Expressions: v0alpha1.RecordingRuleExpressionMap{
|
||||
"A": {
|
||||
QueryType: util.Pointer(rule.Data[0].QueryType),
|
||||
DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{Interval: v0alpha1.RecordingRulePromDuration("10s")},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := client.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, created)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user