Skip to content

Commit 05976ac

Browse files
committed
[RFC-0008] Custom Event Metadata from Annotations
Signed-off-by: Matheus Pimenta <[email protected]>
1 parent 8b1d9a1 commit 05976ac

File tree

5 files changed

+277
-74
lines changed

5 files changed

+277
-74
lines changed

docs/spec/v1beta3/alerts.md

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@ metadata:
2828
name: slack
2929
namespace: flux-system
3030
spec:
31-
summary: "Cluster addons impacted in us-east-2"
31+
summary: Cluster addons impacted
3232
providerRef:
3333
name: slack-bot
34+
eventMetadata:
35+
env: prod
36+
cluster: prod-us-east-2
37+
region: us-east-2
3438
eventSeverity: error
3539
eventSources:
3640
- kind: GitRepository
@@ -51,7 +55,7 @@ In the above example:
5155
all GitRepositories and Kustomizations in the `flux-system` namespace.
5256
- When an event with severity `error` is received, the controller posts
5357
a message on Slack channel from `.spec.channel`,
54-
containing the `summary` text and the reconciliation error.
58+
containing the `summary` text, metadata and the reconciliation error.
5559

5660
You can run this example by saving the manifests into `slack-alerts.yaml`.
5761

@@ -78,10 +82,14 @@ An Alert also needs a
7882

7983
### Summary
8084

81-
`.spec.summary` is an optional field to specify a short description of the
82-
impact and affected cluster.
85+
`.spec.summary` is an optional field to specify a short description of the impact.
86+
87+
The summary max length can't be greater than 255 characters.
8388

84-
The summary max length can't be greater than 255 characters.
89+
**Warning:** Support for `.spec.summary` has been deprecated and will be removed in
90+
Alert API v1 GA. If you have any Alerts using this field, the controller will log a
91+
deprecation warning. Please use [object annotations](#event-metadata-from-object-annotations)
92+
for defining alert summary instead.
8593

8694
### Provider reference
8795

@@ -146,10 +154,11 @@ preventing tenants from subscribing to another tenant's events.
146154
### Event metadata
147155

148156
`.spec.eventMetadata` is an optional field for adding metadata to events dispatched by
149-
the controller. This can be used for enhancing the context of the event. If a field
150-
would override one already present on the original event as generated by the emitter,
151-
then the override doesn't happen, i.e. the original value is preserved, and an info
152-
log is printed.
157+
the controller. This can be used for enhancing the context of the event, e.g. with
158+
cluster-level information.
159+
160+
For all the event metadata sources and their precedence order, please refer to
161+
[Event metadata from object annotations](#event-metadata-from-object-annotations).
153162

154163
#### Example
155164

@@ -168,9 +177,68 @@ spec:
168177
inclusionList:
169178
- ".*succeeded.*"
170179
eventMetadata:
171-
app.kubernetes.io/env: "production"
172-
app.kubernetes.io/cluster: "my-cluster"
173-
app.kubernetes.io/region: "us-east-1"
180+
env: production
181+
cluster: my-cluster
182+
region: us-east-1
183+
```
184+
185+
### Event metadata from object annotations
186+
187+
Event metadata has four sources. They are listed below in order of precedence,
188+
from lowest to highest:
189+
190+
1. User-defined metadata on Flux objects, set with the `event.toolkit.fluxcd.io/`
191+
prefix in the keys of the object's `.metadata.annotations`.
192+
2. User-defined metadata on the Alert object, set with [`.spec.eventMetadata`](#event-metadata).
193+
3. User-defined summary on the Alert object, set with [`.spec.summary`](#summary) (deprecated, see docs).
194+
4. Controller-defined metadata, set with the `<controller group>.toolkit.fluxcd.io/`
195+
prefix in the metadata keys of the event payload.
196+
197+
If there are any metadata key conflicts between the sources, the higher
198+
precedence source will override the lower precedence source, and a warning
199+
log and Kubernetes event will be emitted.
200+
201+
#### Example
202+
203+
```yaml
204+
---
205+
apiVersion: notification.toolkit.fluxcd.io/v1beta3
206+
kind: Alert
207+
metadata:
208+
name: <name>
209+
spec:
210+
eventSources:
211+
- kind: HelmRelease
212+
name: '*'
213+
eventMetadata:
214+
env: production
215+
cluster: my-cluster
216+
region: us-east-1
217+
---
218+
apiVersion: helm.toolkit.fluxcd.io/v2
219+
kind: HelmRelease
220+
metadata:
221+
name: my-webapp
222+
annotations:
223+
event.toolkit.fluxcd.io/summary: "my-webapp impacted. Playbook: <URL to playbook>"
224+
event.toolkit.fluxcd.io/deploymentID: e076e315-5a48-41c3-81c8-8d8bdee7d74d
225+
spec:
226+
... # fields omitted for brevity
227+
```
228+
229+
In the above example, the event payload dispatched by the controller will look like this
230+
(most fields omitted for highlighting the metadata):
231+
232+
```json
233+
{
234+
"metadata": {
235+
"env": "production",
236+
"cluster": "my-cluster",
237+
"region": "us-east-1",
238+
"summary": "my-webapp impacted. Playbook: <URL to playbook>",
239+
"deploymentID": "e076e315-5a48-41c3-81c8-8d8bdee7d74d"
240+
}
241+
}
174242
```
175243

176244
### Event severity

internal/server/event_handlers.go

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"net/http"
2525
"net/url"
2626
"regexp"
27+
"slices"
2728
"strings"
2829
"time"
2930

@@ -256,7 +257,7 @@ func (s *EventServer) getNotificationParams(ctx context.Context, event *eventv1.
256257
}
257258

258259
notification := *event.DeepCopy()
259-
s.enhanceEventWithAlertMetadata(ctx, &notification, alert)
260+
s.combineEventMetadata(ctx, &notification, alert)
260261

261262
return sender, &notification, token, provider.GetTimeout(), nil
262263
}
@@ -418,30 +419,90 @@ func (s *EventServer) eventMatchesAlertSource(ctx context.Context, event *eventv
418419
return sel.Matches(labels.Set(obj.GetLabels()))
419420
}
420421

421-
// enhanceEventWithAlertMetadata enhances the event with Alert metadata.
422-
func (s *EventServer) enhanceEventWithAlertMetadata(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert) {
423-
meta := event.Metadata
424-
if meta == nil {
425-
meta = make(map[string]string)
422+
// combineEventMetadata combines all the sources of metadata for the event
423+
// according to the precedence order defined in RFC 0008. From lowest to
424+
// highest precedence, the sources are:
425+
//
426+
// 1) Event metadata keys prefixed with the Event API Group stripped of the prefix.
427+
//
428+
// 2) Alert .spec.eventMetadata with the keys as they are.
429+
//
430+
// 3) Alert .spec.summary with the key "summary".
431+
//
432+
// 4) Event metadata keys prefixed with the involved object's API Group stripped of the prefix.
433+
//
434+
// At the end of the process key conflicts are detected and a single
435+
// info-level log is emitted to warn users about all the conflicts,
436+
// but only if at least one conflict is found.
437+
func (s *EventServer) combineEventMetadata(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert) {
438+
const (
439+
sourceEventGroup = "involved object annotations"
440+
sourceAlertEventMetadata = "Alert object .spec.eventMetadata"
441+
sourceAlertSummary = "Alert object .spec.summary"
442+
sourceObjectGroup = "involved object controller metadata"
443+
444+
summaryKey = "summary"
445+
)
446+
447+
l := log.FromContext(ctx)
448+
metadata := make(map[string]string)
449+
metadataSources := make(map[string][]string)
450+
451+
// 1) Event metadata keys prefixed with the Event API Group stripped of the prefix.
452+
eventGroupPrefix := "event.toolkit.fluxcd.io/" // TODO: use constant from github.com/fluxcd/pkg/apis/event when available
453+
for k, v := range event.Metadata {
454+
if strings.HasPrefix(k, eventGroupPrefix) {
455+
key := strings.TrimPrefix(k, eventGroupPrefix)
456+
metadata[key] = v
457+
metadataSources[key] = append(metadataSources[key], sourceEventGroup)
458+
}
426459
}
427460

428-
for key, value := range alert.Spec.EventMetadata {
429-
if _, alreadyPresent := meta[key]; !alreadyPresent {
430-
meta[key] = value
431-
} else {
432-
log.FromContext(ctx).
433-
Info("metadata key found in the existing set of metadata", "key", key)
434-
s.Eventf(alert, corev1.EventTypeWarning, "MetadataAppendFailed",
435-
"metadata key found in the existing set of metadata for '%s' in %s", key, involvedObjectString(event.InvolvedObject))
436-
}
461+
// 2) Alert .spec.eventMetadata with the keys as they are.
462+
for k, v := range alert.Spec.EventMetadata {
463+
metadata[k] = v
464+
metadataSources[k] = append(metadataSources[k], sourceAlertEventMetadata)
437465
}
438466

467+
// 3) Alert .spec.summary with the key "summary".
439468
if alert.Spec.Summary != "" {
440-
meta["summary"] = alert.Spec.Summary
469+
metadata[summaryKey] = alert.Spec.Summary
470+
metadataSources[summaryKey] = append(metadataSources[summaryKey], sourceAlertSummary)
471+
l.Info("warning: specifying alert summary cert via '.spec.summary' is deprecated, please use '.spec.eventMetadata.summary' instead")
472+
}
473+
474+
// 4) Event metadata keys prefixed with the involved object's API Group stripped of the prefix.
475+
objectGroupPrefix := event.InvolvedObject.GroupVersionKind().Group + "/"
476+
for k, v := range event.Metadata {
477+
if strings.HasPrefix(k, objectGroupPrefix) {
478+
key := strings.TrimPrefix(k, objectGroupPrefix)
479+
metadata[key] = v
480+
metadataSources[key] = append(metadataSources[key], sourceObjectGroup)
481+
}
482+
}
483+
484+
// Detect key conflicts and emit warnings if any.
485+
type keyConflict struct {
486+
Key string `json:"key"`
487+
Sources []string `json:"sources"`
488+
}
489+
var conflictingKeys []*keyConflict
490+
conflictEventAnnotations := make(map[string]string)
491+
for key, sources := range metadataSources {
492+
if len(sources) > 1 {
493+
conflictingKeys = append(conflictingKeys, &keyConflict{key, sources})
494+
conflictEventAnnotations[key] = strings.Join(sources, ", ")
495+
}
496+
}
497+
if len(conflictingKeys) > 0 {
498+
const msg = "metadata key conflicts detected (please refer to the Alert API docs and Flux RFC 0008 for more information)"
499+
slices.SortFunc(conflictingKeys, func(a, b *keyConflict) int { return strings.Compare(a.Key, b.Key) })
500+
l.Info("warning: "+msg, "conflictingKeys", conflictingKeys)
501+
s.AnnotatedEventf(alert, conflictEventAnnotations, corev1.EventTypeWarning, "MetadataAppendFailed", "%s", msg)
441502
}
442503

443-
if len(meta) > 0 {
444-
event.Metadata = meta
504+
if len(metadata) > 0 {
505+
event.Metadata = metadata
445506
}
446507
}
447508

@@ -450,7 +511,9 @@ func excludeInternalMetadata(event *eventv1.Event) {
450511
if len(event.Metadata) == 0 {
451512
return
452513
}
453-
excludeList := []string{eventv1.MetaTokenKey}
514+
objectGroup := event.InvolvedObject.GetObjectKind().GroupVersionKind().Group
515+
tokenKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaTokenKey)
516+
excludeList := []string{tokenKey}
454517
for _, key := range excludeList {
455518
delete(event.Metadata, key)
456519
}

0 commit comments

Comments
 (0)