Alerting: Support for imported Templates (#114196)

* refactor template service to contstruct notification template in one place, get provenance before creating and calculate resource version after.
* refactor get by UID and name

* introduce template kind in NotificationTemplate
* introduce includeImported flag and use in the k8s api
* support imported templates
* add kind to template uid
* tests for imported templates
* update API model
* set kind to default templates
This commit is contained in:
Yuri Tseretyan
2025-12-17 20:26:22 +00:00
committed by GitHub
parent 370d5c2dc2
commit db3503fb32
16 changed files with 706 additions and 210 deletions
@@ -0,0 +1,129 @@
package templateGroup
import (
"context"
"embed"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util/testutil"
)
//go:embed test-data/*.*
var testData embed.FS
func TestIntegrationImportedTemplates(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{
featuremgmt.FlagAlertingImportAlertmanagerAPI,
},
})
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
cliCfg := helper.Org1.Admin.NewRestConfig()
alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
configYaml, err := testData.ReadFile(path.Join("test-data", "imported.yaml"))
require.NoError(t, err)
identifier := "test-create-get-config"
mergeMatchers := "_imported=true"
headers := map[string]string{
"Content-Type": "application/yaml",
"X-Grafana-Alerting-Config-Identifier": identifier,
"X-Grafana-Alerting-Merge-Matchers": mergeMatchers,
}
var amConfig apimodels.AlertmanagerUserConfig
require.NoError(t, yaml.Unmarshal(configYaml, &amConfig))
response := alertingApi.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
require.Equal(t, "success", response.Status)
templates, err := client.List(context.Background(), metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, templates.Items, 3)
require.Equal(t, v0alpha1.DefaultTemplateTitle, templates.Items[0].Spec.Title)
require.Equal(t, "imported", templates.Items[1].Spec.Title)
require.Equal(t, "template", templates.Items[2].Spec.Title)
t.Run("should be correct kind", func(t *testing.T) {
assert.Equal(t,
v0alpha1.TemplateGroupSpec{
Title: "imported",
Content: amConfig.TemplateFiles["imported"],
Kind: v0alpha1.TemplateGroupTemplateKindMimir,
}, templates.Items[1].Spec)
assert.Equal(t,
v0alpha1.TemplateGroupSpec{
Title: "template",
Content: amConfig.TemplateFiles["template"],
Kind: v0alpha1.TemplateGroupTemplateKindMimir,
}, templates.Items[2].Spec)
})
t.Run("should be provisioned", func(t *testing.T) {
for _, tpl := range templates.Items[1:] {
assert.EqualValues(t, models.ProvenanceConvertedPrometheus, tpl.GetProvenanceStatus())
}
})
t.Run("should not be able to update", func(t *testing.T) {
tpl := templates.Items[1]
tpl.Spec.Content = "new content"
_, err := client.Update(context.Background(), &tpl, metav1.UpdateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
t.Run("should not be able to delete", func(t *testing.T) {
err := client.Delete(context.Background(), templates.Items[1].Name, metav1.DeleteOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
t.Run("should not conflict with Grafana kind", func(t *testing.T) {
tpl := v0alpha1.TemplateGroup{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: templates.Items[1].Spec,
}
tpl.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana
created, err := client.Create(context.Background(), &tpl, metav1.CreateOptions{})
require.NoError(t, err)
assert.NotEqual(t, templates.Items[1].Name, created.Name)
})
t.Run("sort by kind and then name", func(t *testing.T) {
templates, err := client.List(context.Background(), metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, templates.Items, 4)
assert.Equal(t, v0alpha1.DefaultTemplateTitle, templates.Items[0].Spec.Title)
assert.Equal(t, "imported", templates.Items[1].Spec.Title)
assert.Equal(t, v0alpha1.TemplateGroupTemplateKindGrafana, templates.Items[1].Spec.Kind)
assert.Equal(t, "imported", templates.Items[2].Spec.Title)
assert.Equal(t, v0alpha1.TemplateGroupTemplateKindMimir, templates.Items[2].Spec.Kind)
assert.Equal(t, "template", templates.Items[3].Spec.Title)
})
}
@@ -54,6 +54,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "templateGroup",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
@@ -112,6 +113,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
require.Equal(t, v0alpha1.TemplateGroupSpec{
Title: v0alpha1.DefaultTemplateTitle,
Content: defaultDefn.Template,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
}, actual.Spec)
defaultTemplateGroup = actual
})
@@ -226,6 +228,7 @@ func TestIntegrationAccessControl(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: fmt.Sprintf("template-group-1-%s", tc.user.Identity.GetLogin()),
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
expected.SetProvenanceStatus("")
@@ -385,6 +388,7 @@ func TestIntegrationProvisioning(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "template-group-1",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}, v1.CreateOptions{})
require.NoError(t, err)
@@ -428,6 +432,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "template-group-1",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
@@ -510,6 +515,7 @@ func TestIntegrationPatch(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "template-group",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
@@ -568,6 +574,7 @@ func TestIntegrationListSelector(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "test1",
Content: `{{ define "test1" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
template1, err := adminClient.Create(ctx, template1, v1.CreateOptions{})
@@ -580,6 +587,7 @@ func TestIntegrationListSelector(t *testing.T) {
Spec: v0alpha1.TemplateGroupSpec{
Title: "test2",
Content: `{{ define "test2" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
},
}
template2, err = adminClient.Create(ctx, template2, v1.CreateOptions{})
@@ -655,3 +663,37 @@ func TestIntegrationListSelector(t *testing.T) {
require.NotEqualf(t, templates.DefaultTemplateName, list.Items[1].Name, "Expected non-default template but got %s", list.Items[1].Name)
})
}
func TestIntegrationKinds(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
newTemplate := &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.TemplateGroupSpec{
Title: "templateGroup",
Content: `{{ define "test" }} test {{ end }}`,
Kind: v0alpha1.TemplateGroupTemplateKindMimir,
},
}
t.Run("should not let create Mimir template", func(t *testing.T) {
_, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
t.Run("should not let change kind", func(t *testing.T) {
newTemplate.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana
created, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
require.NoError(t, err)
created.Spec.Kind = v0alpha1.TemplateGroupTemplateKindMimir
_, err = client.Update(ctx, created, v1.UpdateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
}
@@ -0,0 +1,15 @@
alertmanager_config: |
route:
receiver: sinkhole
group_by:
- alertname
receivers:
- name: sinkhole
template_files:
imported: |
{{ define "imported" }}
{{ end }}
template: |
{{ define "template" }}
{{ template "imported" . }}
{{ end }}
@@ -3783,18 +3783,29 @@
"type": "object",
"required": [
"title",
"content"
"content",
"kind"
],
"properties": {
"content": {
"type": "string"
},
"kind": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupTemplateKind"
},
"title": {
"type": "string"
}
},
"additionalProperties": false
},
"com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupTemplateKind": {
"type": "string",
"enum": [
"grafana",
"mimir"
]
},
"com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": {
"type": "object",
"required": [