mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
[Alerting]: Assign UUID to grafana receivers (#34241)
* [Alerting]: Assign UUID to grafana receivers * Apply suggestions from code review * Add test for updating invalid receiver Co-authored-by: Domas <domasx2@gmail.com>
This commit is contained in:
parent
2e7ccf0e42
commit
11243dec14
@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -13,7 +12,6 @@ import (
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@ -86,6 +84,13 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response
|
||||
for _, pr := range recv.PostableGrafanaReceivers.GrafanaManagedReceivers {
|
||||
secureFields := make(map[string]bool, len(pr.SecureSettings))
|
||||
for k := range pr.SecureSettings {
|
||||
decryptedValue, err := pr.GetDecryptedSecret(k)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to decrypt stored secure setting: %s", k), err)
|
||||
}
|
||||
if decryptedValue == "" {
|
||||
continue
|
||||
}
|
||||
secureFields[k] = true
|
||||
}
|
||||
gr := apimodels.GettableGrafanaReceiver{
|
||||
@ -191,49 +196,43 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "failed to load lastest configuration", err)
|
||||
}
|
||||
currentReceiverMap := currentConfig.GetGrafanaReceiverMap()
|
||||
|
||||
// Copy the previously known secure settings
|
||||
for i, r := range body.AlertmanagerConfig.Receivers {
|
||||
for j, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers {
|
||||
if len(currentConfig.AlertmanagerConfig.Receivers) <= i { // this is a receiver we don't have any stored for - skip it.
|
||||
if gr.UID == "" { // new receiver
|
||||
continue
|
||||
}
|
||||
cr := currentConfig.AlertmanagerConfig.Receivers[i]
|
||||
|
||||
if len(cr.PostableGrafanaReceivers.GrafanaManagedReceivers) <= j { // this is a receiver we don't have anything stored for - skip it.
|
||||
continue
|
||||
cgmr, ok := currentReceiverMap[gr.UID]
|
||||
if !ok {
|
||||
// it tries to update a receiver that didn't previously exist
|
||||
return response.Error(http.StatusBadRequest, fmt.Sprintf("unknown receiver: %s", gr.UID), nil)
|
||||
}
|
||||
cgmr := cr.PostableGrafanaReceivers.GrafanaManagedReceivers[j]
|
||||
|
||||
//TODO: We use the name and type to match current stored receivers againt sent ones, but we should ideally use something unique e.g. UUID
|
||||
if cgmr.Name == gr.Name && cgmr.Type == gr.Type {
|
||||
// frontend sends only the secure settings that have to be updated
|
||||
// therefore we have to copy from the last configuration only those secure settings not included in the request
|
||||
for key, storedValue := range cgmr.SecureSettings {
|
||||
_, ok := body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings[key]
|
||||
if !ok {
|
||||
decodeValue, err := base64.StdEncoding.DecodeString(storedValue)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to decode stored secure setting: %s", key), err)
|
||||
}
|
||||
decryptedValue, err := util.Decrypt(decodeValue, setting.SecretKey)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to decrypt stored secure setting: %s", key), err)
|
||||
}
|
||||
|
||||
if body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings == nil {
|
||||
body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings = make(map[string]string, len(cgmr.SecureSettings))
|
||||
}
|
||||
|
||||
body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings[key] = string(decryptedValue)
|
||||
// frontend sends only the secure settings that have to be updated
|
||||
// therefore we have to copy from the last configuration only those secure settings not included in the request
|
||||
for key := range cgmr.SecureSettings {
|
||||
_, ok := body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings[key]
|
||||
if !ok {
|
||||
decryptedValue, err := cgmr.GetDecryptedSecret(key)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to decrypt stored secure setting: %s", key), err)
|
||||
}
|
||||
|
||||
if body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings == nil {
|
||||
body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings = make(map[string]string, len(cgmr.SecureSettings))
|
||||
}
|
||||
|
||||
body.AlertmanagerConfig.Receivers[i].PostableGrafanaReceivers.GrafanaManagedReceivers[j].SecureSettings[key] = decryptedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := body.EncryptSecureSettings(); err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "failed to encrypt receiver secrets", err)
|
||||
if err := body.ProcessConfig(); err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "failed to post process Alertmanager configuration", err)
|
||||
}
|
||||
|
||||
if err := srv.am.SaveAndApplyConfig(&body); err != nil {
|
||||
|
@ -245,7 +245,24 @@ func (c *PostableUserConfig) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PostableUserConfig) EncryptSecureSettings() error {
|
||||
// GetGrafanaReceiverMap returns a map that associates UUIDs to grafana receivers
|
||||
func (c *PostableUserConfig) GetGrafanaReceiverMap() map[string]*PostableGrafanaReceiver {
|
||||
UIDs := make(map[string]*PostableGrafanaReceiver)
|
||||
for _, r := range c.AlertmanagerConfig.Receivers {
|
||||
switch r.Type() {
|
||||
case GrafanaReceiverType:
|
||||
for _, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers {
|
||||
UIDs[gr.UID] = gr
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
return UIDs
|
||||
}
|
||||
|
||||
// ProcessConfig parses grafana receivers, encrypts secrets and assigns UUIDs (if they are missing)
|
||||
func (c *PostableUserConfig) ProcessConfig() error {
|
||||
seenUIDs := make(map[string]struct{})
|
||||
// encrypt secure settings for storing them in DB
|
||||
for _, r := range c.AlertmanagerConfig.Receivers {
|
||||
switch r.Type() {
|
||||
@ -258,6 +275,21 @@ func (c *PostableUserConfig) EncryptSecureSettings() error {
|
||||
}
|
||||
gr.SecureSettings[k] = base64.StdEncoding.EncodeToString(encryptedData)
|
||||
}
|
||||
if gr.UID == "" {
|
||||
retries := 5
|
||||
for i := 0; i < retries; i++ {
|
||||
gen := util.GenerateShortUID()
|
||||
_, ok := seenUIDs[gen]
|
||||
if !ok {
|
||||
gr.UID = gen
|
||||
break
|
||||
}
|
||||
}
|
||||
if gr.UID == "" {
|
||||
return fmt.Errorf("all %d attempts to generate UID for receiver have failed; please retry", retries)
|
||||
}
|
||||
}
|
||||
seenUIDs[gr.UID] = struct{}{}
|
||||
}
|
||||
default:
|
||||
}
|
||||
@ -353,6 +385,21 @@ func (c *GettableUserConfig) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(tmp)
|
||||
}
|
||||
|
||||
// GetGrafanaReceiverMap returns a map that associates UUIDs to grafana receivers
|
||||
func (c *GettableUserConfig) GetGrafanaReceiverMap() map[string]*GettableGrafanaReceiver {
|
||||
UIDs := make(map[string]*GettableGrafanaReceiver)
|
||||
for _, r := range c.AlertmanagerConfig.Receivers {
|
||||
switch r.Type() {
|
||||
case GrafanaReceiverType:
|
||||
for _, gr := range r.GettableGrafanaReceivers.GrafanaManagedReceivers {
|
||||
UIDs[gr.UID] = gr
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
return UIDs
|
||||
}
|
||||
|
||||
type GettableApiAlertingConfig struct {
|
||||
Config `yaml:",inline"`
|
||||
|
||||
@ -521,6 +568,22 @@ type PostableGrafanaReceiver struct {
|
||||
SecureSettings map[string]string `json:"secureSettings"`
|
||||
}
|
||||
|
||||
func (r *PostableGrafanaReceiver) GetDecryptedSecret(key string) (string, error) {
|
||||
storedValue, ok := r.SecureSettings[key]
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
decodeValue, err := base64.StdEncoding.DecodeString(storedValue)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
decryptedValue, err := util.Decrypt(decodeValue, setting.SecretKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decryptedValue), nil
|
||||
}
|
||||
|
||||
type ReceiverType int
|
||||
|
||||
const (
|
||||
|
@ -1,12 +1,15 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -74,6 +77,7 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
||||
store := testinfra.SetUpDatabase(t, dir)
|
||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||
alertConfigURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
||||
generatedUID := ""
|
||||
|
||||
// create a new configuration that has a secret
|
||||
{
|
||||
@ -105,9 +109,56 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
||||
resp := postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
|
||||
require.JSONEq(t, `{"message":"configuration created"}`, getBody(t, resp.Body))
|
||||
}
|
||||
// Then, update the recipient
|
||||
|
||||
// Try to update a receiver with unknown UID
|
||||
{
|
||||
// Then, update the recipient
|
||||
payload := `
|
||||
{
|
||||
"template_files": {},
|
||||
"alertmanager_config": {
|
||||
"route": {
|
||||
"receiver": "slack.receiver"
|
||||
},
|
||||
"templates": null,
|
||||
"receivers": [{
|
||||
"name": "slack.receiver",
|
||||
"grafana_managed_receiver_configs": [{
|
||||
"settings": {
|
||||
"recipient": "#unified-alerting-test-but-updated"
|
||||
},
|
||||
"secureFields": {
|
||||
"url": true
|
||||
},
|
||||
"type": "slack",
|
||||
"name": "slack.receiver",
|
||||
"disableResolveMessage": false,
|
||||
"uid": "invalid"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint
|
||||
require.JSONEq(t, `{"message": "unknown receiver: invalid"}`, getBody(t, resp.Body))
|
||||
}
|
||||
|
||||
// The secure settings must be present
|
||||
{
|
||||
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
||||
var c definitions.GettableUserConfig
|
||||
bb := getBody(t, resp.Body)
|
||||
err := json.Unmarshal([]byte(bb), &c)
|
||||
require.NoError(t, err)
|
||||
m := c.GetGrafanaReceiverMap()
|
||||
assert.Len(t, m, 1)
|
||||
for k := range m {
|
||||
generatedUID = m[k].UID
|
||||
}
|
||||
|
||||
// Then, update the recipient
|
||||
payload := fmt.Sprintf(`
|
||||
{
|
||||
"template_files": {},
|
||||
"alertmanager_config": {
|
||||
@ -126,20 +177,22 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
||||
},
|
||||
"type": "slack",
|
||||
"name": "slack.receiver",
|
||||
"disableResolveMessage": false
|
||||
"disableResolveMessage": false,
|
||||
"uid": %q
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
`
|
||||
resp := postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
|
||||
`, generatedUID)
|
||||
|
||||
resp = postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
|
||||
require.JSONEq(t, `{"message": "configuration created"}`, getBody(t, resp.Body))
|
||||
}
|
||||
|
||||
// The secure settings must be present
|
||||
{
|
||||
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
||||
require.JSONEq(t, `
|
||||
require.JSONEq(t, fmt.Sprintf(`
|
||||
{
|
||||
"template_files": {},
|
||||
"alertmanager_config": {
|
||||
@ -150,7 +203,7 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
||||
"receivers": [{
|
||||
"name": "slack.receiver",
|
||||
"grafana_managed_receiver_configs": [{
|
||||
"uid": "",
|
||||
"uid": %q,
|
||||
"name": "slack.receiver",
|
||||
"type": "slack",
|
||||
"disableResolveMessage": false,
|
||||
@ -164,6 +217,6 @@ func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
|
||||
}]
|
||||
}
|
||||
}
|
||||
`, getBody(t, resp.Body))
|
||||
`, generatedUID), getBody(t, resp.Body))
|
||||
}
|
||||
}
|
||||
|
@ -67,8 +67,9 @@ func TestNotificationChannels(t *testing.T) {
|
||||
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
|
||||
resp := getRequest(t, alertsURL, http.StatusOK) // nolint
|
||||
b := getBody(t, resp.Body)
|
||||
re := regexp.MustCompile(`"uid":"([\w|-]*)"`)
|
||||
e := getExpAlertmanagerConfigFromAPI(mockChannel.server.Addr)
|
||||
require.JSONEq(t, e, b)
|
||||
require.JSONEq(t, e, string(re.ReplaceAll([]byte(b), []byte(`"uid":""`))))
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -224,6 +224,9 @@ function formChannelValuesToGrafanaChannelConfig(
|
||||
disableResolveMessage:
|
||||
values.disableResolveMessage ?? existing?.disableResolveMessage ?? defaults.disableResolveMessage,
|
||||
};
|
||||
if (existing) {
|
||||
channel.uid = existing.uid;
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,7 @@ export type WebhookConfig = {
|
||||
};
|
||||
|
||||
export type GrafanaManagedReceiverConfig = {
|
||||
uid?: string;
|
||||
disableResolveMessage: boolean;
|
||||
secureFields?: Record<string, boolean>;
|
||||
secureSettings?: Record<string, unknown>;
|
||||
|
Loading…
Reference in New Issue
Block a user