Alerting: Integration tests for Receiver API (#90632)

---------

Co-authored-by: Matthew Jacobson <matthew.jacobson@grafana.com>
This commit is contained in:
Yuri Tseretyan
2024-08-29 22:27:26 -04:00
committed by GitHub
co-authored by Matthew Jacobson
parent c4d804a964
commit ce64d79027
5 changed files with 1075 additions and 2 deletions
@@ -7,8 +7,8 @@ var (
ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one."))
ErrReceiverNotFound = errutil.NotFound("alerting.notifications.receivers.notFound", errutil.WithPublicMessage("Receiver not found"))
ErrReceiverExists = errutil.BadRequest("alerting.notifications.receivers.exists", errutil.WithPublicMessage("Receiver with this name already exists. Use a different name or update an existing one."))
ErrReceiverInvalid = errutil.Conflict("alerting.notifications.receivers.invalid").MustTemplate(
ErrReceiverExists = errutil.Conflict("alerting.notifications.receivers.exists", errutil.WithPublicMessage("Receiver with this name already exists. Use a different name or update an existing one."))
ErrReceiverInvalid = errutil.BadRequest("alerting.notifications.receivers.invalid").MustTemplate(
"Invalid receiver: '{{ .Public.Reason }}'",
errutil.WithPublic("Invalid receiver: '{{ .Public.Reason }}'"),
)
+38
View File
@@ -878,6 +878,44 @@ func (a apiClient) EnsureReceiver(t *testing.T, receiver apimodels.EmbeddedConta
require.Equalf(t, http.StatusAccepted, status, body)
}
func (a apiClient) ExportReceiver(t *testing.T, name string, format string, decrypt bool) string {
t.Helper()
u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/contact-points/export", a.url))
require.NoError(t, err)
q := url.Values{}
q.Set("name", name)
q.Set("format", format)
q.Set("decrypt", fmt.Sprintf("%v", decrypt))
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
require.NoError(t, err)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
requireStatusCode(t, http.StatusOK, resp.StatusCode, string(body))
return string(body)
}
func (a apiClient) ExportReceiverTyped(t *testing.T, name string, decrypt bool) apimodels.ContactPointExport {
t.Helper()
response := a.ExportReceiver(t, name, "json", decrypt)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(response), &export))
require.Len(t, export.ContactPoints, 1)
return export.ContactPoints[0]
}
func (a apiClient) GetAlertmanagerConfigWithStatus(t *testing.T) (apimodels.GettableUserConfig, int, string) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/config/api/v1/alerts", a.url), nil)
@@ -0,0 +1,956 @@
package receivers
import (
"context"
"embed"
"encoding/json"
"fmt"
"maps"
"net/http"
"path"
"slices"
"sort"
"strings"
"testing"
"github.com/grafana/alerting/notify"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/generated/clientset/versioned"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert/api"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
)
//go:embed test-data/*.*
var testData embed.FS
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func getTestHelper(t *testing.T) *apis.K8sTestHelper {
return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{
featuremgmt.FlagAlertingApiServer,
},
})
}
func TestIntegrationResourceIdentifier(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := getTestHelper(t)
adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig())
require.NoError(t, err)
client := adminK8sClient.NotificationsV0alpha1().Receivers("default")
newResource := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "Test-Receiver",
Integrations: nil,
},
}
t.Run("create should fail if object name is specified", func(t *testing.T) {
resource := newResource.DeepCopy()
resource.Name = "new-receiver"
_, err := client.Create(ctx, resource, v1.CreateOptions{})
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
})
var resourceID string
t.Run("create should succeed and provide resource name", func(t *testing.T) {
actual, err := client.Create(ctx, newResource, v1.CreateOptions{})
require.NoError(t, err)
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
resourceID = actual.Name
})
t.Run("resource should be available by the identifier", func(t *testing.T) {
actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
require.NoError(t, err)
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
require.Equal(t, newResource.Spec, actual.Spec)
})
// TODO uncomment when renaming is supported
// t.Run("update should rename receiver if name in the specification changes", func(t *testing.T) {
// existing, err := client.Get(ctx, resourceID, v1.GetOptions{})
// require.NoError(t, err)
//
// updated := existing.DeepCopy()
// updated.Spec.Title = "another-newReceiver"
//
// 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)
// })
}
func TestIntegrationAccessControl(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := getTestHelper(t)
org1 := helper.Org1
type testCase struct {
user apis.User
canRead bool
canUpdate bool
canCreate bool
canDelete bool
}
// region users
unauthorized := helper.CreateUser("unauthorized", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{})
reader := helper.CreateUser("reader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(accesscontrol.ActionAlertingReceiversRead),
})
secretsReader := helper.CreateUser("secretsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(accesscontrol.ActionAlertingReceiversReadSecrets),
})
creator := helper.CreateUser("creator", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversRead,
accesscontrol.ActionAlertingReceiversCreate,
),
})
updater := helper.CreateUser("updater", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversRead,
accesscontrol.ActionAlertingReceiversUpdate,
),
})
deleter := helper.CreateUser("deleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversRead,
accesscontrol.ActionAlertingReceiversDelete,
),
})
legacyReader := helper.CreateUser("legacyReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsRead,
},
},
})
legacyWriter := helper.CreateUser("legacyWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsRead,
accesscontrol.ActionAlertingNotificationsWrite,
},
},
})
// endregion
testCases := []testCase{
{
user: unauthorized,
canRead: false,
canUpdate: false,
canCreate: false,
canDelete: false,
},
{
user: org1.Admin,
canRead: true,
canUpdate: true,
canCreate: true,
canDelete: true,
},
{
user: org1.Editor,
canRead: true,
canUpdate: true,
canCreate: true,
canDelete: true,
},
{
user: org1.Viewer,
canRead: true,
},
{
user: reader,
canRead: true,
},
{
user: secretsReader,
canRead: true,
},
{
user: creator,
canRead: true,
canCreate: true,
},
{
user: updater,
canRead: true,
canUpdate: true,
},
{
user: deleter,
canRead: true,
canDelete: true,
},
{
user: legacyReader,
canRead: true,
},
{
user: legacyWriter,
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
},
}
admin := org1.Admin
adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig())
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().Receivers("default")
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
k8sClient, err := versioned.NewForConfig(tc.user.NewRestConfig())
require.NoError(t, err)
client := k8sClient.NotificationsV0alpha1().Receivers("default")
var expected = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: fmt.Sprintf("receiver-1-%s", tc.user.Identity.GetLogin()),
Integrations: nil,
},
}
d, err := json.Marshal(expected)
require.NoError(t, err)
if tc.canCreate {
t.Run("should be able to create receiver", func(t *testing.T) {
newReceiver := expected
actual, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.Equal(t, expected.Spec, actual.Spec)
t.Run("should fail if already exists", func(t *testing.T) {
_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
require.Truef(t, errors.IsConflict(err), "expected bad request but got %s", err)
})
expected = actual
})
} else {
t.Run("should be forbidden to create", func(t *testing.T) {
_, err := client.Create(ctx, expected, v1.CreateOptions{})
require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
})
// create resource to proceed with other tests
expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.NotNil(t, expected)
}
if tc.canRead {
t.Run("should be able to list receivers", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 2) // default + created
})
t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, expected, got)
t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to list receivers", func(t *testing.T) {
_, err := client.List(ctx, v1.ListOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
}
updatedExpected := expected.DeepCopy()
updatedExpected.Spec.Integrations = append(updatedExpected.Spec.Integrations, createIntegration(t, "email"))
d, err = json.Marshal(updatedExpected)
require.NoError(t, err)
if tc.canUpdate {
t.Run("should be able to update receiver", func(t *testing.T) {
updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
expected = updated
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
up := updatedExpected.DeepCopy()
up.Name = "notFound"
_, err := client.Update(ctx, up, v1.UpdateOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to update receiver", func(t *testing.T) {
_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
up := updatedExpected.DeepCopy()
up.Name = "notFound"
_, err := client.Update(ctx, up, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
}
deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
if tc.canDelete {
t.Run("should be able to delete receiver", func(t *testing.T) {
err := client.Delete(ctx, expected.Name, deleteOptions)
require.NoError(t, err)
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to delete receiver", func(t *testing.T) {
err := client.Delete(ctx, expected.Name, deleteOptions)
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
}
if tc.canRead {
t.Run("should get empty list if no receivers", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1)
})
}
})
}
}
func TestIntegrationProvisioning(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := getTestHelper(t)
org := helper.Org1
admin := org.Admin
adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig())
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().Receivers("default")
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)
created, err := adminClient.Create(ctx, &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "test-receiver-1",
Integrations: []v0alpha1.Integration{
createIntegration(t, "email"),
},
},
}, v1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, "none", created.GetProvenanceStatus())
t.Run("should provide provenance status", func(t *testing.T) {
require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{
UID: *created.Spec.Integrations[0].Uid,
}, admin.Identity.GetOrgID(), "API"))
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, "API", got.GetProvenanceStatus())
})
t.Run("should not let update if provisioned", func(t *testing.T) {
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
updated := got.DeepCopy()
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
_, err = adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
t.Run("should not let delete if provisioned", func(t *testing.T) {
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
}
func TestIntegrationOptimisticConcurrency(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := getTestHelper(t)
adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig())
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().Receivers("default")
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "receiver-1",
Integrations: nil,
},
}
created, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, created)
require.NotEmpty(t, created.ResourceVersion)
t.Run("should forbid if version does not match", func(t *testing.T) {
updated := created.DeepCopy()
updated.ResourceVersion = "test"
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
})
t.Run("should update if version matches", func(t *testing.T) {
updated := created.DeepCopy()
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.NoError(t, err)
for i, integration := range actualUpdated.Spec.Integrations {
updated.Spec.Integrations[i].Uid = integration.Uid
}
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
})
t.Run("should fail to update if version is empty", func(t *testing.T) {
updated := created.DeepCopy()
updated.ResourceVersion = ""
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "webhook"))
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) // TODO Change that? K8s returns 400 instead.
})
t.Run("should fail to delete if version does not match", func(t *testing.T) {
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer("something"),
},
})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
})
t.Run("should succeed if version matches", func(t *testing.T) {
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer(actual.ResourceVersion),
},
})
require.NoError(t, err)
})
t.Run("should succeed if version is empty", func(t *testing.T) {
actual, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer(actual.ResourceVersion),
},
})
require.NoError(t, err)
})
}
func TestIntegrationPatch(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := getTestHelper(t)
adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig())
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().Receivers("default")
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "receiver",
Integrations: []v0alpha1.Integration{
createIntegration(t, "email"),
createIntegration(t, "webhook"),
createIntegration(t, "sns"),
},
},
}
current, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, current)
t.Run("should patch with json patch", func(t *testing.T) {
current, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
require.NoError(t, err)
index := slices.IndexFunc(current.Spec.Integrations, func(t v0alpha1.Integration) bool {
return t.Type == "webhook"
})
patch := []map[string]any{
{
"op": "remove",
"path": fmt.Sprintf("/spec/integrations/%d/settings/username", index),
},
{
"op": "remove",
"path": fmt.Sprintf("/spec/integrations/%d/secureFields/password", index),
},
{
"op": "replace",
"path": fmt.Sprintf("/spec/integrations/%d/settings/authorization_scheme", index),
"value": "bearer",
},
{
"op": "add",
"path": fmt.Sprintf("/spec/integrations/%d/settings/authorization_credentials", index),
"value": "authz-token",
},
{
"op": "remove",
"path": fmt.Sprintf("/spec/integrations/%d/secureFields/authorization_credentials", index),
},
}
expected := current.Spec.Integrations[index]
expected.Settings.Remove("username")
expected.Settings.Remove("password")
expected.Settings.Set("authorization_scheme", "bearer")
delete(expected.SecureFields, "password")
expected.SecureFields["authorization_credentials"] = true
patchData, err := json.Marshal(patch)
require.NoError(t, err)
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
require.NoError(t, err)
require.EqualValues(t, expected, result.Spec.Integrations[index])
// Use export endpoint because it's the only way to get decrypted secrets fast.
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
export := legacyCli.ExportReceiverTyped(t, current.Spec.Title, true)
// we need special unmarshaler to parse settings
cp, err := api.ContactPointFromContactPointExport(export)
require.NoError(t, err)
assert.Len(t, cp.Sns, 1)
assert.Len(t, cp.Email, 1)
require.Len(t, cp.Webhook, 1)
settings := cp.Webhook[0]
assert.EqualValues(t, "authz-token", *settings.AuthorizationCredentials)
assert.EqualValues(t, "bearer", *settings.AuthorizationScheme)
assert.Nil(t, settings.Password)
assert.Nil(t, settings.User)
})
}
func TestIntegrationReferentialIntegrity(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
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)
// Prepare environment and create notification policy and rule that use time receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
var ruleGroup definitions.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup))
folderUID := "test-folder"
legacyCli.CreateFolder(t, folderUID, "TEST")
_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
adminK8sClient, err := versioned.NewForConfig(cliCfg)
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().Receivers("default")
receivers, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, receivers.Items, 2)
idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
return interval.Spec.Title == "user-defined"
})
receiver := receivers.Items[idx]
// TODO uncomment when renaming is enabled
// currentRoute := legacyCli.GetRoute(t)
// currentRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
// 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("Update", func(t *testing.T) {
// t.Run("should rename all references if name changes", func(t *testing.T) {
// renamed := receiver.DeepCopy()
// expectedTitle := renamed.Spec.Title + "-new"
// renamed.Spec.Title += expectedTitle
//
// 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 {
// assert.Equalf(t, expectedTitle, rule.GrafanaManagedAlert.NotificationSettings.Receiver, "receiver in rule %d should have been renamed but it did not", idx)
// }
//
// updatedRoute := legacyCli.GetRoute(t)
// for _, route := range updatedRoute.Routes {
// assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
// }
// receiver = *actual
// })
//
// t.Run("should fail if at least one resource is provisioned", func(t *testing.T) {
// require.NoError(t, err)
// renamed := receiver.DeepCopy()
// renamed.Spec.Title += util.GenerateShortUID()
//
// t.Run("provisioned route", func(t *testing.T) {
// require.NoError(t, db.SetProvenance(ctx, &currentRoute, orgID, "API"))
// t.Cleanup(func() {
// require.NoError(t, db.DeleteProvenance(ctx, &currentRoute, 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("Delete", func(t *testing.T) {
t.Run("should fail to delete if receiver is used in rule and routes", func(t *testing.T) {
err := adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
t.Run("should fail to delete if receiver is used in only rule", func(t *testing.T) {
route := legacyCli.GetRoute(t)
route.Routes[0].Receiver = ""
legacyCli.UpdateRoute(t, route, true)
err = adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
})
}
func TestIntegrationCRUD(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
helper := getTestHelper(t)
adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig())
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().Receivers("default")
var defaultReceiver *v0alpha1.Receiver
t.Run("should list the default receiver", func(t *testing.T) {
items, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
assert.Len(t, items.Items, 1)
defaultReceiver = &items.Items[0]
assert.Equal(t, "grafana-default-email", defaultReceiver.Spec.Title)
assert.NotEmpty(t, defaultReceiver.UID)
assert.NotEmpty(t, defaultReceiver.Name)
assert.NotEmpty(t, defaultReceiver.ResourceVersion)
defaultReceiver, err = adminClient.Get(ctx, defaultReceiver.Name, v1.GetOptions{})
require.NoError(t, err)
assert.NotEmpty(t, defaultReceiver.UID)
assert.NotEmpty(t, defaultReceiver.Name)
assert.NotEmpty(t, defaultReceiver.ResourceVersion)
assert.Len(t, defaultReceiver.Spec.Integrations, 1)
})
t.Run("should be able to update default receiver", func(t *testing.T) {
require.NotNil(t, defaultReceiver)
newDefault := defaultReceiver.DeepCopy()
newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, "line"))
updatedReceiver, err := adminClient.Update(ctx, newDefault, v1.UpdateOptions{})
require.NoError(t, err)
expected := newDefault.DeepCopy()
expected.Spec.Integrations[0].Uid = updatedReceiver.Spec.Integrations[0].Uid // default integration does not have UID before first update
lineIntegration := expected.Spec.Integrations[1]
lineIntegration.SecureFields = map[string]bool{
"token": true,
}
lineIntegration.Settings.Remove("token")
assert.Equal(t, "LINE", updatedReceiver.Spec.Integrations[1].Type) // this type is in the schema but not in backend
lineIntegration.Type = "LINE"
lineIntegration.Uid = updatedReceiver.Spec.Integrations[1].Uid
expected.Spec.Integrations[1] = lineIntegration
assert.Equal(t, expected.Spec, updatedReceiver.Spec)
})
t.Run("should fail to create receiver with the existing name", func(t *testing.T) {
newReceiver := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: defaultReceiver.Spec.Title,
Integrations: nil,
},
}
_, err := adminClient.Create(ctx, newReceiver, v1.CreateOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
t.Run("should not let delete default receiver", func(t *testing.T) {
err := adminClient.Delete(ctx, defaultReceiver.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
var receiver *v0alpha1.Receiver
t.Run("should correctly persist all known integrations", func(t *testing.T) {
integrations := make([]v0alpha1.Integration, 0, len(notify.AllKnownConfigsForTesting))
keysIter := maps.Keys(notify.AllKnownConfigsForTesting)
keys := slices.Collect(keysIter)
sort.Strings(keys)
for _, key := range keys {
integrations = append(integrations, createIntegration(t, key))
}
receiver, err = adminClient.Create(ctx, &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "all-receivers",
Integrations: integrations,
},
}, v1.CreateOptions{})
require.NoError(t, err)
require.Len(t, receiver.Spec.Integrations, len(integrations))
// Use export endpoint because it's the only way to get decrypted secrets fast.
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
export := legacyCli.ExportReceiverTyped(t, receiver.Spec.Title, true)
for _, integration := range export.Receivers {
expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)] // to lower because there is LINE that is in different casing in API
assert.JSONEqf(t, expected.Config, string(integration.Settings), "integration %s", integration.Type)
}
})
t.Run("should be able read what it is created", func(t *testing.T) {
get, err := adminClient.Get(ctx, receiver.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, receiver, get)
t.Run("should return secrets in secureFields but not settings", func(t *testing.T) {
for _, integration := range get.Spec.Integrations {
t.Run(integration.Type, func(t *testing.T) {
secretFields, err := channels_config.GetSecretKeysForContactPointType(integration.Type)
require.NoError(t, err)
for _, field := range secretFields {
assert.Contains(t, integration.SecureFields, field)
assert.Truef(t, integration.SecureFields[field], "secure field should be always true")
value, ok, err := unstructured.NestedString(integration.Settings.Object, strings.Split(field, ".")...)
assert.NoErrorf(t, err, "failed to read field %s from settings", field)
assert.Falsef(t, ok, "secret field %s should not be in settings, value [%s]", field, value)
}
})
}
})
})
t.Run("should fail to persist receiver with invalid config", func(t *testing.T) {
keysIter := maps.Keys(notify.AllKnownConfigsForTesting)
keys := slices.Collect(keysIter)
sort.Strings(keys)
for _, key := range keys {
t.Run(key, func(t *testing.T) {
integration := createIntegration(t, key)
// Make the integration invalid, so it fails to create. This is usually done by sending empty settings.
clear(integration.Settings.Object)
if key == "webex" {
// Webex integration is special case and passes validation without any settings so we instead set an invalid URL.
integration.Settings.Set("api_url", "(*^$*^%!@#$*()")
}
receiver, err = adminClient.Create(ctx, &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: fmt.Sprintf("invalid-%s", key),
Integrations: []v0alpha1.Integration{integration},
},
}, v1.CreateOptions{})
require.Errorf(t, err, "Expected error but got successful result: %v", receiver)
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err)
})
}
})
}
func createIntegration(t *testing.T, integrationType string) v0alpha1.Integration {
cfg, ok := notify.AllKnownConfigsForTesting[integrationType]
require.Truef(t, ok, "no known config for integration type %s", integrationType)
settings := common.Unstructured{}
require.NoError(t, settings.UnmarshalJSON([]byte(cfg.Config)))
return v0alpha1.Integration{
Settings: settings,
Type: cfg.NotifierType,
DisableResolveMessage: util.Pointer(false),
}
}
func createWildcardPermission(actions ...string) resourcepermissions.SetResourcePermissionCommand {
return resourcepermissions.SetResourcePermissionCommand{
Actions: actions,
Resource: "receivers",
ResourceAttribute: "uid",
ResourceID: "*",
}
}
@@ -0,0 +1,45 @@
{
"template_files": {},
"template_file_provenances": {},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email",
"routes": [
{
"receiver": "user-defined",
"object_matchers": [
[
"test",
"=",
"test"
]
]
}
]
},
"receivers": [
{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [
{
"type": "email",
"settings": {
"addresses": "example@email.com"
}
}
]
},
{
"name": "user-defined",
"grafana_managed_receiver_configs": [
{
"type": "email",
"settings": {
"addresses": "example@email.com"
}
}
]
}
]
}
}
@@ -0,0 +1,34 @@
{
"name": "Group1",
"interval": "1m",
"rules": [
{
"for": "0",
"labels": {
"label1": "test-label"
},
"annotations": {
"annotation": "test-annotation"
},
"grafana_alert": {
"title": "Rule1",
"condition": "A",
"data": [
{
"refId": "A",
"datasourceUid": "__expr__",
"model": {
"expression": "0 > 0",
"type": "math"
}
}
],
"no_data_state": "NoData",
"exec_err_state": "Alerting",
"notification_settings": {
"receiver": "user-defined"
}
}
}
]
}