mirror of https://github.com/grafana/grafana
Alerting: Provisioning API - Contact points (#47197)
parent
5fb80498b1
commit
388ecb4037
@ -0,0 +1,157 @@ |
||||
package definitions |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" |
||||
) |
||||
|
||||
// swagger:route GET /api/provisioning/contact-points provisioning RouteGetContactpoints
|
||||
//
|
||||
// Get all the contact points.
|
||||
//
|
||||
// Responses:
|
||||
// 200: Route
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route POST /api/provisioning/contact-points provisioning RoutePostContactpoints
|
||||
//
|
||||
// Create a contact point.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route PUT /api/provisioning/contact-points provisioning RoutePutContactpoints
|
||||
//
|
||||
// Update an existing contact point.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route DELETE /api/provisioning/contact-points/{ID} provisioning RouteDeleteContactpoints
|
||||
//
|
||||
// Delete a contact point.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:parameters RoutePostContactpoints RoutePutContactpoints
|
||||
type ContactPointPayload struct { |
||||
// in:body
|
||||
Body EmbeddedContactPoint |
||||
} |
||||
|
||||
// EmbeddedContactPoint is the contact point type that is used
|
||||
// by grafanas embedded alertmanager implementation.
|
||||
type EmbeddedContactPoint struct { |
||||
// UID is the unique identifier of the contact point. This will be
|
||||
// automatically set be the Grafana.
|
||||
UID string `json:"uid"` |
||||
// Name is used as grouping key in the UI. Contact points with the
|
||||
// same name will be grouped in the UI.
|
||||
Name string `json:"name" binding:"required"` |
||||
Type string `json:"type" binding:"required"` |
||||
Settings *simplejson.Json `json:"settings" binding:"required"` |
||||
DisableResolveMessage bool `json:"disableResolveMessage"` |
||||
Provenance string `json:"provanance"` |
||||
} |
||||
|
||||
const RedactedValue = "[REDACTED]" |
||||
|
||||
func (e *EmbeddedContactPoint) Valid(decryptFunc channels.GetDecryptedValueFn) error { |
||||
if e.Type == "" { |
||||
return fmt.Errorf("type should not be an empty string") |
||||
} |
||||
if e.Settings == nil { |
||||
return fmt.Errorf("settings should not be empty") |
||||
} |
||||
factory, exists := channels.Factory(e.Type) |
||||
if !exists { |
||||
return fmt.Errorf("unknown type '%s'", e.Type) |
||||
} |
||||
cfg, _ := channels.NewFactoryConfig(&channels.NotificationChannelConfig{ |
||||
Settings: e.Settings, |
||||
Type: e.Type, |
||||
}, nil, decryptFunc, nil) |
||||
if _, err := factory(cfg); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (e *EmbeddedContactPoint) SecretKeys() ([]string, error) { |
||||
switch e.Type { |
||||
case "alertmanager": |
||||
return []string{"basicAuthPassword"}, nil |
||||
case "dingding": |
||||
return []string{}, nil |
||||
case "discord": |
||||
return []string{}, nil |
||||
case "email": |
||||
return []string{}, nil |
||||
case "googlechat": |
||||
return []string{}, nil |
||||
case "kafka": |
||||
return []string{}, nil |
||||
case "line": |
||||
return []string{"token"}, nil |
||||
case "opsgenie": |
||||
return []string{"apiKey"}, nil |
||||
case "pagerduty": |
||||
return []string{"integrationKey"}, nil |
||||
case "pushover": |
||||
return []string{"userKey", "apiToken"}, nil |
||||
case "sensugo": |
||||
return []string{"apiKey"}, nil |
||||
case "slack": |
||||
return []string{"url", "token"}, nil |
||||
case "teams": |
||||
return []string{}, nil |
||||
case "telegram": |
||||
return []string{"bottoken"}, nil |
||||
case "threema": |
||||
return []string{"api_secret"}, nil |
||||
case "victorops": |
||||
return []string{}, nil |
||||
case "webhook": |
||||
return []string{}, nil |
||||
case "wecom": |
||||
return []string{"url"}, nil |
||||
} |
||||
return nil, fmt.Errorf("no secrets configured for type '%s'", e.Type) |
||||
} |
||||
|
||||
func (e *EmbeddedContactPoint) ExtractSecrets() (map[string]string, error) { |
||||
secrets := map[string]string{} |
||||
secretKeys, err := e.SecretKeys() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for _, secretKey := range secretKeys { |
||||
secretValue := e.Settings.Get(secretKey).MustString() |
||||
e.Settings.Del(secretKey) |
||||
secrets[secretKey] = secretValue |
||||
} |
||||
return secrets, nil |
||||
} |
||||
|
||||
func (e *EmbeddedContactPoint) ResourceID() string { |
||||
return e.UID |
||||
} |
||||
|
||||
func (e *EmbeddedContactPoint) ResourceType() string { |
||||
return "contactPoint" |
||||
} |
@ -0,0 +1,402 @@ |
||||
package provisioning |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/base64" |
||||
"encoding/json" |
||||
"fmt" |
||||
"sort" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/store" |
||||
"github.com/grafana/grafana/pkg/services/secrets" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"github.com/prometheus/alertmanager/config" |
||||
) |
||||
|
||||
type ContactPointService struct { |
||||
amStore AMConfigStore |
||||
encryptionService secrets.Service |
||||
provenanceStore ProvisioningStore |
||||
xact TransactionManager |
||||
log log.Logger |
||||
} |
||||
|
||||
func NewContactPointService(store store.AlertingStore, encryptionService secrets.Service, |
||||
provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger) *ContactPointService { |
||||
return &ContactPointService{ |
||||
amStore: store, |
||||
encryptionService: encryptionService, |
||||
provenanceStore: provenanceStore, |
||||
xact: xact, |
||||
log: log, |
||||
} |
||||
} |
||||
|
||||
func (ecp *ContactPointService) GetContactPoints(ctx context.Context, orgID int64) ([]apimodels.EmbeddedContactPoint, error) { |
||||
cfg, _, err := ecp.getCurrentConfig(ctx, orgID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
provenances, err := ecp.provenanceStore.GetProvenances(ctx, orgID, "contactPoint") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
contactPoints := []apimodels.EmbeddedContactPoint{} |
||||
for _, contactPoint := range cfg.GetGrafanaReceiverMap() { |
||||
embeddedContactPoint := apimodels.EmbeddedContactPoint{ |
||||
UID: contactPoint.UID, |
||||
Type: contactPoint.Type, |
||||
Name: contactPoint.Name, |
||||
DisableResolveMessage: contactPoint.DisableResolveMessage, |
||||
Settings: contactPoint.Settings, |
||||
} |
||||
if val, exists := provenances[embeddedContactPoint.UID]; exists && val != "" { |
||||
embeddedContactPoint.Provenance = string(val) |
||||
} |
||||
for k, v := range contactPoint.SecureSettings { |
||||
decryptedValue, err := ecp.decryptValue(v) |
||||
if err != nil { |
||||
ecp.log.Warn("decrypting value failed", "err", err.Error()) |
||||
continue |
||||
} |
||||
if decryptedValue == "" { |
||||
continue |
||||
} |
||||
embeddedContactPoint.Settings.Set(k, apimodels.RedactedValue) |
||||
} |
||||
contactPoints = append(contactPoints, embeddedContactPoint) |
||||
} |
||||
sort.SliceStable(contactPoints, func(i, j int) bool { |
||||
return contactPoints[i].Name < contactPoints[j].Name |
||||
}) |
||||
return contactPoints, nil |
||||
} |
||||
|
||||
// internal only
|
||||
func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, orgID int64, uid string) (apimodels.EmbeddedContactPoint, error) { |
||||
cfg, _, err := ecp.getCurrentConfig(ctx, orgID) |
||||
if err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, err |
||||
} |
||||
for _, receiver := range cfg.GetGrafanaReceiverMap() { |
||||
if receiver.UID != uid { |
||||
continue |
||||
} |
||||
embeddedContactPoint := apimodels.EmbeddedContactPoint{ |
||||
UID: receiver.UID, |
||||
Type: receiver.Type, |
||||
Name: receiver.Name, |
||||
DisableResolveMessage: receiver.DisableResolveMessage, |
||||
Settings: receiver.Settings, |
||||
} |
||||
for k, v := range receiver.SecureSettings { |
||||
decryptedValue, err := ecp.decryptValue(v) |
||||
if err != nil { |
||||
ecp.log.Warn("decrypting value failed", "err", err.Error()) |
||||
continue |
||||
} |
||||
if decryptedValue == "" { |
||||
continue |
||||
} |
||||
embeddedContactPoint.Settings.Set(k, decryptedValue) |
||||
} |
||||
return embeddedContactPoint, nil |
||||
} |
||||
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point with uid '%s' not found", uid) |
||||
} |
||||
|
||||
func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID int64, |
||||
contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) (apimodels.EmbeddedContactPoint, error) { |
||||
if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, fmt.Errorf("contact point is not valid: %w", err) |
||||
} |
||||
|
||||
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID) |
||||
if err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, err |
||||
} |
||||
|
||||
extractedSecrets, err := contactPoint.ExtractSecrets() |
||||
if err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, err |
||||
} |
||||
|
||||
for k, v := range extractedSecrets { |
||||
encryptedValue, err := ecp.encryptValue(v) |
||||
if err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, err |
||||
} |
||||
extractedSecrets[k] = encryptedValue |
||||
} |
||||
|
||||
contactPoint.UID = util.GenerateShortUID() |
||||
grafanaReceiver := &apimodels.PostableGrafanaReceiver{ |
||||
UID: contactPoint.UID, |
||||
Name: contactPoint.Name, |
||||
Type: contactPoint.Type, |
||||
DisableResolveMessage: contactPoint.DisableResolveMessage, |
||||
Settings: contactPoint.Settings, |
||||
SecureSettings: extractedSecrets, |
||||
} |
||||
|
||||
receiverFound := false |
||||
for _, receiver := range cfg.AlertmanagerConfig.Receivers { |
||||
if receiver.Name == contactPoint.Name { |
||||
receiver.PostableGrafanaReceivers.GrafanaManagedReceivers = append(receiver.PostableGrafanaReceivers.GrafanaManagedReceivers, grafanaReceiver) |
||||
receiverFound = true |
||||
} |
||||
} |
||||
|
||||
if !receiverFound { |
||||
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{ |
||||
Receiver: config.Receiver{ |
||||
Name: grafanaReceiver.Name, |
||||
}, |
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ |
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{grafanaReceiver}, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
data, err := json.Marshal(cfg) |
||||
if err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, err |
||||
} |
||||
|
||||
err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error { |
||||
err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ |
||||
AlertmanagerConfiguration: string(data), |
||||
FetchedConfigurationHash: fetchedHash, |
||||
ConfigurationVersion: "v1", |
||||
Default: false, |
||||
OrgID: orgID, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
adapter := provenanceOrgAdapter{ |
||||
inner: &contactPoint, |
||||
orgID: orgID, |
||||
} |
||||
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
contactPoint.Provenance = string(provenance) |
||||
return nil |
||||
}) |
||||
if err != nil { |
||||
return apimodels.EmbeddedContactPoint{}, err |
||||
} |
||||
for k := range extractedSecrets { |
||||
contactPoint.Settings.Set(k, apimodels.RedactedValue) |
||||
} |
||||
return contactPoint, nil |
||||
} |
||||
|
||||
func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error { |
||||
// set all redacted values with the latest known value from the store
|
||||
rawContactPoint, err := ecp.getContactPointDecrypted(ctx, orgID, contactPoint.UID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
secretKeys, err := contactPoint.SecretKeys() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, secretKey := range secretKeys { |
||||
secretValue := contactPoint.Settings.Get(secretKey).MustString() |
||||
if secretValue == apimodels.RedactedValue { |
||||
contactPoint.Settings.Set(secretKey, rawContactPoint.Settings.Get(secretKey).MustString()) |
||||
} |
||||
} |
||||
// validate merged values
|
||||
if err := contactPoint.Valid(ecp.encryptionService.GetDecryptedValue); err != nil { |
||||
return err |
||||
} |
||||
// check that provenance is not changed in a invalid way
|
||||
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, provenanceOrgAdapter{ |
||||
inner: &contactPoint, |
||||
orgID: orgID, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { |
||||
return fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance) |
||||
} |
||||
// transform to internal model
|
||||
extractedSecrets, err := contactPoint.ExtractSecrets() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for k, v := range extractedSecrets { |
||||
encryptedValue, err := ecp.encryptValue(v) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
extractedSecrets[k] = encryptedValue |
||||
} |
||||
mergedReceiver := &apimodels.PostableGrafanaReceiver{ |
||||
UID: contactPoint.UID, |
||||
Name: contactPoint.Name, |
||||
Type: contactPoint.Type, |
||||
DisableResolveMessage: contactPoint.DisableResolveMessage, |
||||
Settings: contactPoint.Settings, |
||||
SecureSettings: extractedSecrets, |
||||
} |
||||
// save to store
|
||||
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, receiver := range cfg.AlertmanagerConfig.Receivers { |
||||
if receiver.Name == contactPoint.Name { |
||||
receiverNotFound := true |
||||
for i, grafanaReceiver := range receiver.GrafanaManagedReceivers { |
||||
if grafanaReceiver.UID == mergedReceiver.UID { |
||||
receiverNotFound = false |
||||
receiver.GrafanaManagedReceivers[i] = mergedReceiver |
||||
break |
||||
} |
||||
} |
||||
if receiverNotFound { |
||||
return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID) |
||||
} |
||||
} |
||||
} |
||||
data, err := json.Marshal(cfg) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { |
||||
err = ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ |
||||
AlertmanagerConfiguration: string(data), |
||||
FetchedConfigurationHash: fetchedHash, |
||||
ConfigurationVersion: "v1", |
||||
Default: false, |
||||
OrgID: orgID, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
adapter := provenanceOrgAdapter{ |
||||
inner: &contactPoint, |
||||
orgID: orgID, |
||||
} |
||||
err = ecp.provenanceStore.SetProvenance(ctx, adapter, provenance) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
contactPoint.Provenance = string(provenance) |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID int64, uid string) error { |
||||
cfg, fetchedHash, err := ecp.getCurrentConfig(ctx, orgID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// Indicates if the full contact point is removed or just one of the
|
||||
// configurations, as a contactpoint can consist of any number of
|
||||
// configurations.
|
||||
fullRemoval := false |
||||
// Name of the contact point that will be removed, might be used if a
|
||||
// full removal is done to check if it's referenced in any route.
|
||||
name := "" |
||||
for i, receiver := range cfg.AlertmanagerConfig.Receivers { |
||||
for j, grafanaReceiver := range receiver.GrafanaManagedReceivers { |
||||
if grafanaReceiver.UID == uid { |
||||
name = grafanaReceiver.Name |
||||
receiver.GrafanaManagedReceivers = append(receiver.GrafanaManagedReceivers[:j], receiver.GrafanaManagedReceivers[j+1:]...) |
||||
// if this was the last receiver we removed, we remove the whole receiver
|
||||
if len(receiver.GrafanaManagedReceivers) == 0 { |
||||
fullRemoval = true |
||||
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:i], cfg.AlertmanagerConfig.Receivers[i+1:]...) |
||||
} |
||||
break |
||||
} |
||||
} |
||||
} |
||||
if fullRemoval && isContactPointInUse(name, []*apimodels.Route{cfg.AlertmanagerConfig.Route}) { |
||||
return fmt.Errorf("contact point '%s' is currently used by a notification policy", name) |
||||
} |
||||
data, err := json.Marshal(cfg) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { |
||||
err := ecp.provenanceStore.DeleteProvenance(ctx, provenanceOrgAdapter{ |
||||
inner: &apimodels.EmbeddedContactPoint{ |
||||
UID: uid, |
||||
}, |
||||
orgID: orgID, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ecp.amStore.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ |
||||
AlertmanagerConfiguration: string(data), |
||||
FetchedConfigurationHash: fetchedHash, |
||||
ConfigurationVersion: "v1", |
||||
Default: false, |
||||
OrgID: orgID, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func (ecp *ContactPointService) getCurrentConfig(ctx context.Context, orgID int64) (*apimodels.PostableUserConfig, string, error) { |
||||
query := &models.GetLatestAlertmanagerConfigurationQuery{ |
||||
OrgID: orgID, |
||||
} |
||||
err := ecp.amStore.GetLatestAlertmanagerConfiguration(ctx, query) |
||||
if err != nil { |
||||
return nil, "", err |
||||
} |
||||
cfg, err := DeserializeAlertmanagerConfig([]byte(query.Result.AlertmanagerConfiguration)) |
||||
if err != nil { |
||||
return nil, "", err |
||||
} |
||||
return cfg, query.Result.ConfigurationHash, nil |
||||
} |
||||
|
||||
func isContactPointInUse(name string, routes []*apimodels.Route) bool { |
||||
if len(routes) == 0 { |
||||
return false |
||||
} |
||||
for _, route := range routes { |
||||
if route.Receiver == name { |
||||
return true |
||||
} |
||||
if isContactPointInUse(name, route.Routes) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (ecp *ContactPointService) decryptValue(value string) (string, error) { |
||||
decodeValue, err := base64.StdEncoding.DecodeString(value) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
decryptedValue, err := ecp.encryptionService.Decrypt(context.Background(), decodeValue) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return string(decryptedValue), nil |
||||
} |
||||
|
||||
func (ecp *ContactPointService) encryptValue(value string) (string, error) { |
||||
encryptedData, err := ecp.encryptionService.Encrypt(context.Background(), []byte(value), secrets.WithoutScope()) |
||||
if err != nil { |
||||
return "", fmt.Errorf("failed to encrypt secure settings: %w", err) |
||||
} |
||||
return base64.StdEncoding.EncodeToString(encryptedData), nil |
||||
} |
@ -0,0 +1,195 @@ |
||||
package provisioning |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/secrets" |
||||
"github.com/grafana/grafana/pkg/services/secrets/database" |
||||
"github.com/grafana/grafana/pkg/services/secrets/manager" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestContactPointService(t *testing.T) { |
||||
sqlStore := sqlstore.InitTestDB(t) |
||||
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) |
||||
t.Run("service gets contact points from AM config", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
|
||||
require.Len(t, cps, 1) |
||||
require.Equal(t, "email receiver", cps[0].Name) |
||||
}) |
||||
|
||||
t.Run("service stitches contact point into org's AM config", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
newCp := createTestContactPoint() |
||||
|
||||
_, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Len(t, cps, 2) |
||||
require.Equal(t, "test-contact-point", cps[1].Name) |
||||
require.Equal(t, "slack", cps[1].Type) |
||||
}) |
||||
|
||||
t.Run("default provenance of contact points is none", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[0].Provenance)) |
||||
}) |
||||
|
||||
t.Run("it's possible to update provenance from none to API", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
newCp := createTestContactPoint() |
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, newCp.UID, cps[1].UID) |
||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance)) |
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err = sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, newCp.UID, cps[1].UID) |
||||
require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance)) |
||||
}) |
||||
|
||||
t.Run("it's possible to update provenance from none to File", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
newCp := createTestContactPoint() |
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, newCp.UID, cps[1].UID) |
||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance)) |
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err = sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, newCp.UID, cps[1].UID) |
||||
require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance)) |
||||
}) |
||||
|
||||
t.Run("it's not possible to update provenance from File to API", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
newCp := createTestContactPoint() |
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, newCp.UID, cps[1].UID) |
||||
require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance)) |
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) |
||||
require.Error(t, err) |
||||
}) |
||||
|
||||
t.Run("it's not possible to update provenance from API to File", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
newCp := createTestContactPoint() |
||||
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) |
||||
require.NoError(t, err) |
||||
|
||||
cps, err := sut.GetContactPoints(context.Background(), 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, newCp.UID, cps[1].UID) |
||||
require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance)) |
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile) |
||||
require.Error(t, err) |
||||
}) |
||||
|
||||
t.Run("service respects concurrency token when updating", func(t *testing.T) { |
||||
sut := createContactPointServiceSut(secretsService) |
||||
newCp := createTestContactPoint() |
||||
q := models.GetLatestAlertmanagerConfigurationQuery{ |
||||
OrgID: 1, |
||||
} |
||||
err := sut.amStore.GetLatestAlertmanagerConfiguration(context.Background(), &q) |
||||
require.NoError(t, err) |
||||
expectedConcurrencyToken := q.Result.ConfigurationHash |
||||
|
||||
_, err = sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) |
||||
require.NoError(t, err) |
||||
|
||||
fake := sut.amStore.(*fakeAMConfigStore) |
||||
intercepted := fake.lastSaveCommand |
||||
require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash) |
||||
}) |
||||
} |
||||
|
||||
func TestContactPointInUse(t *testing.T) { |
||||
result := isContactPointInUse("test", []*definitions.Route{ |
||||
{ |
||||
Receiver: "not-test", |
||||
Routes: []*definitions.Route{ |
||||
{ |
||||
Receiver: "not-test", |
||||
}, |
||||
{ |
||||
Receiver: "test", |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
require.True(t, result) |
||||
result = isContactPointInUse("test", []*definitions.Route{ |
||||
{ |
||||
Receiver: "not-test", |
||||
Routes: []*definitions.Route{ |
||||
{ |
||||
Receiver: "not-test", |
||||
}, |
||||
{ |
||||
Receiver: "not-test", |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
require.False(t, result) |
||||
} |
||||
|
||||
func createContactPointServiceSut(secretService secrets.Service) *ContactPointService { |
||||
return &ContactPointService{ |
||||
amStore: newFakeAMConfigStore(), |
||||
provenanceStore: newFakeProvisioningStore(), |
||||
xact: newNopTransactionManager(), |
||||
encryptionService: secretService, |
||||
log: log.NewNopLogger(), |
||||
} |
||||
} |
||||
|
||||
func createTestContactPoint() definitions.EmbeddedContactPoint { |
||||
settings, _ := simplejson.NewJson([]byte(`{"recipient":"value_recipient","token":"value_token"}`)) |
||||
return definitions.EmbeddedContactPoint{ |
||||
Name: "test-contact-point", |
||||
Type: "slack", |
||||
Settings: settings, |
||||
} |
||||
} |
Loading…
Reference in new issue