Alerting: Annotation CanUse for receiver resource (#110839)
* add origin to receiver * populate origin of the receiver * set CanUse to false if origin is not Grafana * set provenance if origin is imported * set Grafana origin by default in conversion API * set canUse annotation * reject update\delete operations on resources with origin other than Grafana * fail to create with wrong origin
This commit is contained in:
@@ -3,3 +3,4 @@ package v0alpha1
|
||||
const InternalPrefix = "grafana.com/"
|
||||
const ProvenanceStatusAnnotationKey = InternalPrefix + "provenance"
|
||||
const ProvenanceStatusNone = "none"
|
||||
const CanUseAnnotationKey = InternalPrefix + "canUse"
|
||||
|
||||
@@ -35,6 +35,13 @@ func (o *Receiver) SetAccessControl(action string) {
|
||||
o.Annotations[AccessControlAnnotation(action)] = "true"
|
||||
}
|
||||
|
||||
func (o *Receiver) SetCanUse(canUse bool) {
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string, 1)
|
||||
}
|
||||
o.Annotations[CanUseAnnotationKey] = fmt.Sprintf("%v", canUse)
|
||||
}
|
||||
|
||||
// AccessControlAnnotation returns the key for the access control annotation for the given action.
|
||||
// Ex. grafana.com/access/canDelete.
|
||||
func AccessControlAnnotation(action string) string {
|
||||
|
||||
@@ -101,6 +101,7 @@ func convertToK8sResource(
|
||||
rules = append(rules, rule.UID)
|
||||
}
|
||||
r.SetInUse(metadata.InUseByRoutes, rules)
|
||||
r.SetCanUse(metadata.CanUse)
|
||||
}
|
||||
r.UID = gapiutil.CalculateClusterWideUID(r)
|
||||
return r, nil
|
||||
@@ -120,6 +121,7 @@ func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[str
|
||||
Integrations: make([]*ngmodels.Integration, 0, len(receiver.Spec.Integrations)),
|
||||
Version: receiver.ResourceVersion,
|
||||
Provenance: ngmodels.ProvenanceNone,
|
||||
Origin: ngmodels.ResourceOriginGrafana, // Set to Grafana by default.
|
||||
}
|
||||
storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations))
|
||||
for _, integration := range receiver.Spec.Integrations {
|
||||
|
||||
@@ -37,8 +37,20 @@ type GetReceiversQuery struct {
|
||||
type ReceiverMetadata struct {
|
||||
InUseByRules []AlertRuleKey
|
||||
InUseByRoutes int
|
||||
// CanUse is true if the receiver can be used in routes and rules.
|
||||
CanUse bool
|
||||
}
|
||||
|
||||
// ResourceOrigin represents the origin or source of the resource.
|
||||
type ResourceOrigin string
|
||||
|
||||
const (
|
||||
// ResourceOriginGrafana indicates that the resource is in the Grafana configuration
|
||||
ResourceOriginGrafana ResourceOrigin = "grafana"
|
||||
// ResourceOriginImported indicates that the resource is from the imported configuration
|
||||
ResourceOriginImported ResourceOrigin = "imported"
|
||||
)
|
||||
|
||||
// Receiver is the domain model representation of a receiver / contact point.
|
||||
type Receiver struct {
|
||||
UID string
|
||||
@@ -46,6 +58,7 @@ type Receiver struct {
|
||||
Integrations []*Integration
|
||||
Provenance Provenance
|
||||
Version string
|
||||
Origin ResourceOrigin
|
||||
}
|
||||
|
||||
func (r *Receiver) Clone() Receiver {
|
||||
@@ -54,6 +67,7 @@ func (r *Receiver) Clone() Receiver {
|
||||
Name: r.Name,
|
||||
Provenance: r.Provenance,
|
||||
Version: r.Version,
|
||||
Origin: r.Origin,
|
||||
}
|
||||
|
||||
if r.Integrations != nil {
|
||||
|
||||
@@ -378,6 +378,7 @@ func TestReceiver_Fingerprint(t *testing.T) {
|
||||
fingerprint := baseReceiver.Fingerprint()
|
||||
excludedFields := map[string]struct{}{
|
||||
"Version": {},
|
||||
"Origin": {},
|
||||
}
|
||||
|
||||
reflectVal := reflect.ValueOf(&completelyDifferentReceiver).Elem()
|
||||
|
||||
@@ -1180,6 +1180,7 @@ func ReceiverGen(mutators ...Mutator[Receiver]) func() Receiver {
|
||||
Name: name,
|
||||
Integrations: []*Integration{&integration},
|
||||
Provenance: ProvenanceNone,
|
||||
Origin: ResourceOriginGrafana,
|
||||
}
|
||||
for _, mutator := range mutators {
|
||||
mutator(&c)
|
||||
@@ -1246,6 +1247,12 @@ func (n ReceiverMutators) Decrypted(fn DecryptFn) Mutator[Receiver] {
|
||||
}
|
||||
}
|
||||
|
||||
func (n ReceiverMutators) WithOrigin(origin ResourceOrigin) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
r.Origin = origin
|
||||
}
|
||||
}
|
||||
|
||||
// Integrations
|
||||
|
||||
// CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it.
|
||||
|
||||
@@ -65,19 +65,7 @@ func ReceiverToPostableApiReceiver(r *models.Receiver) (*apimodels.PostableApiRe
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PostableApiReceiversToReceivers(postables []*apimodels.PostableApiReceiver, storedProvenances map[string]models.Provenance) ([]*models.Receiver, error) {
|
||||
receivers := make([]*models.Receiver, 0, len(postables))
|
||||
for _, postable := range postables {
|
||||
r, err := PostableApiReceiverToReceiver(postable, GetReceiverProvenance(storedProvenances, postable))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivers = append(receivers, r)
|
||||
}
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, provenance models.Provenance) (*models.Receiver, error) {
|
||||
func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, provenance models.Provenance, origin models.ResourceOrigin) (*models.Receiver, error) {
|
||||
integrations, err := PostableGrafanaReceiversToIntegrations(postable.GrafanaManagedReceivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,13 +75,18 @@ func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, prov
|
||||
Name: postable.GetName(),
|
||||
Integrations: integrations,
|
||||
Provenance: provenance,
|
||||
Origin: origin,
|
||||
}
|
||||
r.Version = r.Fingerprint()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GetReceiverProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations.
|
||||
func GetReceiverProvenance(storedProvenances map[string]models.Provenance, r *apimodels.PostableApiReceiver) models.Provenance {
|
||||
func GetReceiverProvenance(storedProvenances map[string]models.Provenance, r *apimodels.PostableApiReceiver, origin models.ResourceOrigin) models.Provenance {
|
||||
if origin == models.ResourceOriginImported {
|
||||
return models.ProvenanceConvertedPrometheus
|
||||
}
|
||||
|
||||
if len(r.GrafanaManagedReceivers) == 0 || len(storedProvenances) == 0 {
|
||||
return models.ProvenanceNone
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (rev *ConfigRevision) CreateReceiver(receiver *models.Receiver) (*models.Re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return PostableApiReceiverToReceiver(postable, receiver.Provenance)
|
||||
return PostableApiReceiverToReceiver(postable, receiver.Provenance, receiver.Origin)
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) UpdateReceiver(receiver *models.Receiver) (*models.Receiver, error) {
|
||||
@@ -69,7 +69,7 @@ func (rev *ConfigRevision) UpdateReceiver(receiver *models.Receiver) (*models.Re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return PostableApiReceiverToReceiver(newReceiver, receiver.Provenance)
|
||||
return PostableApiReceiverToReceiver(newReceiver, receiver.Provenance, receiver.Origin)
|
||||
}
|
||||
|
||||
// ReceiverNameUsedByRoutes checks if a receiver name is used in any routes.
|
||||
@@ -89,7 +89,7 @@ func (rev *ConfigRevision) GetReceiver(uid string, prov Provenances) (*models.Re
|
||||
if NameToUid(r.GetName()) != uid {
|
||||
continue
|
||||
}
|
||||
recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r))
|
||||
recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r, models.ResourceOriginGrafana), models.ResourceOriginGrafana)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert receiver %q: %w", r.Name, err)
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (rev *ConfigRevision) GetReceivers(uids []string, prov Provenances) ([]*mod
|
||||
if len(uids) > 0 && !slices.Contains(uids, uid) {
|
||||
continue
|
||||
}
|
||||
recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r))
|
||||
recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r, models.ResourceOriginGrafana), models.ResourceOriginGrafana)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert receiver %q: %w", r.Name, err)
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func DecryptedReceivers(receivers []*definitions.PostableApiReceiver, decryptFn
|
||||
decrypted := make([]*definitions.PostableApiReceiver, len(receivers))
|
||||
for i, r := range receivers {
|
||||
// We don't care about the provenance here, so we pass ProvenanceNone.
|
||||
rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone)
|
||||
rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone, models.ResourceOriginGrafana)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func EncryptedReceivers(receivers []*definitions.PostableApiReceiver, encryptFn
|
||||
encrypted := make([]*definitions.PostableApiReceiver, len(receivers))
|
||||
for i, r := range receivers {
|
||||
// We don't care about the provenance here, so we pass ProvenanceNone.
|
||||
rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone)
|
||||
rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone, models.ResourceOriginGrafana)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ var (
|
||||
"Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}",
|
||||
errutil.WithPublic(`Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}. You must update those resources first using the original provision method.`),
|
||||
)
|
||||
|
||||
ErrReceiverOrigin = errutil.BadRequest("alerting.notifications.receivers.originInvalid").MustTemplate(
|
||||
"Receiver '{{ .Public.Name }} cannot be {{ .Public.Action }}d because it belongs to an imported configuration.",
|
||||
errutil.WithPublic("Receiver '{{ .Public.Name }} cannot be {{ .Public.Action }}d because it belongs to an imported configuration. Finish the import of the configuration first."),
|
||||
)
|
||||
)
|
||||
|
||||
// ReceiverService is the service for managing alertmanager receivers.
|
||||
@@ -271,6 +276,10 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.Origin != models.ResourceOriginGrafana {
|
||||
return makeErrReceiverOrigin(existing, "delete")
|
||||
}
|
||||
|
||||
logger := rs.log.FromContext(ctx).New("receiver", existing.Name, "uid", uid, "version", version, "integrations", existing.GetIntegrationTypes())
|
||||
|
||||
// Check optimistic concurrency.
|
||||
@@ -329,7 +338,9 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive
|
||||
if err := rs.authz.AuthorizeCreate(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.Origin != models.ResourceOriginGrafana {
|
||||
return nil, makeErrReceiverOrigin(r, "create")
|
||||
}
|
||||
revision, err := rs.cfgStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -429,6 +440,10 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existing.Origin != models.ResourceOriginGrafana {
|
||||
return nil, makeErrReceiverOrigin(existing, "update")
|
||||
}
|
||||
|
||||
if err := rs.provenanceValidator(existing.Provenance, r.Provenance); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -538,6 +553,7 @@ func (rs *ReceiverService) InUseMetadata(ctx context.Context, orgID int64, recei
|
||||
results[rcv.GetUID()] = models.ReceiverMetadata{
|
||||
InUseByRoutes: receiverUses[rcv.Name],
|
||||
InUseByRules: byReceiver[rcv.Name],
|
||||
CanUse: rcv.Origin == models.ResourceOriginGrafana, // Only receivers from the Grafana configuration can be used.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,6 +700,10 @@ func makeErrReceiverDependentResourcesProvenance(usedByRoutes bool, rules []mode
|
||||
})
|
||||
}
|
||||
|
||||
func makeErrReceiverOrigin(r *models.Receiver, action string) error {
|
||||
return ErrReceiverOrigin.Build(errutil.TemplateData{Public: map[string]interface{}{"Action": action, "Name": r.Name}})
|
||||
}
|
||||
|
||||
func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Context, orgID int64, revision *legacy_storage.ConfigRevision, oldName, newName string, receiverProvenance models.Provenance) error {
|
||||
ctx, span := rs.tracer.Start(ctx, "alerting.receivers.rename-dependent-resources", trace.WithAttributes(
|
||||
attribute.String("oldName", oldName),
|
||||
|
||||
@@ -404,6 +404,12 @@ func TestReceiverService_Create(t *testing.T) {
|
||||
), models.ReceiverMuts.Encrypted(models.Base64Enrypt)),
|
||||
expectedProvenances: map[string]models.Provenance{generated(0): models.ProvenanceNone, generated(1): models.ProvenanceNone}, // Mark UIDs as generated so that test will insert generated UID.
|
||||
},
|
||||
{
|
||||
name: "create receiver with non-Grafana origin fails",
|
||||
user: writer,
|
||||
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithOrigin(models.ResourceOriginImported)),
|
||||
expectedErr: ErrReceiverOrigin,
|
||||
},
|
||||
{
|
||||
name: "create integration with invalid UID fails",
|
||||
user: writer,
|
||||
@@ -1479,18 +1485,22 @@ func TestReceiverService_InUseMetadata(t *testing.T) {
|
||||
legacy_storage.NameToUid("receiver1"): {
|
||||
InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule1uid"}},
|
||||
InUseByRoutes: 2,
|
||||
CanUse: true,
|
||||
},
|
||||
legacy_storage.NameToUid("receiver2"): {
|
||||
InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule1uid"}, {OrgID: 1, UID: "rule2uid"}},
|
||||
InUseByRoutes: 1,
|
||||
CanUse: true,
|
||||
},
|
||||
legacy_storage.NameToUid("receiver3"): {
|
||||
InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule2uid"}},
|
||||
InUseByRoutes: 2,
|
||||
CanUse: true,
|
||||
},
|
||||
legacy_storage.NameToUid("receiver4"): {
|
||||
InUseByRules: []models.AlertRuleKey{},
|
||||
InUseByRoutes: 1,
|
||||
CanUse: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -608,6 +608,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
// Set expected metadata.
|
||||
expectedWithMetadata := expected.Copy().(*v0alpha1.Receiver)
|
||||
expectedWithMetadata.SetInUse(0, nil)
|
||||
expectedWithMetadata.SetCanUse(true)
|
||||
if tc.canUpdate {
|
||||
expectedWithMetadata.SetAccessControl("canWrite")
|
||||
}
|
||||
@@ -1294,6 +1295,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
receiver.SetAccessControl("canReadSecrets")
|
||||
receiver.SetAccessControl("canAdmin")
|
||||
receiver.SetInUse(0, nil)
|
||||
receiver.SetCanUse(true)
|
||||
|
||||
// Use export endpoint because it's the only way to get decrypted secrets fast.
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
|
||||
Reference in New Issue
Block a user