mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Preserve webhook mentions in list API and resolved notifications (#1118)
Back-port v5 fixes5997fd81fand0a7b93a84to v6: - GetWebhooks list response now includes the configured mention so the UI shows it after reload instead of blanking it. - sendResolvedWebhook now assigns data.Mention (v6 set it for grouped/ firing webhooks but dropped it on resolved), and the Discord/Slack/Teams/ Mattermost ResolvedPayloadTemplate strings gained {{if .Mention}} guards. Without these, a configured @everyone/@channel was silently omitted from resolved/cleared notifications. Adds list-API and per-service resolved mention regression tests.
This commit is contained in:
parent
4bfd211a76
commit
85ec355268
5 changed files with 107 additions and 2 deletions
|
|
@ -318,6 +318,11 @@ func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Reques
|
|||
whMap["template"] = webhook.Template
|
||||
}
|
||||
|
||||
// Include the configured mention so the UI can render it after reload (#1118)
|
||||
if webhook.Mention != "" {
|
||||
whMap["mention"] = webhook.Mention
|
||||
}
|
||||
|
||||
maskedWebhooks[i] = whMap
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ func TestNotificationHandlers(t *testing.T) {
|
|||
Name: "Test Webhook",
|
||||
URL: "https://example.com",
|
||||
Headers: map[string]string{"Authorization": "Bearer token"},
|
||||
Mention: "@everyone",
|
||||
},
|
||||
}
|
||||
mockManager.On("GetWebhooks").Return(webhooks).Once()
|
||||
|
|
@ -370,6 +371,7 @@ func TestNotificationHandlers(t *testing.T) {
|
|||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, 1, len(resp))
|
||||
assert.Equal(t, "wh1", resp[0]["id"])
|
||||
assert.Equal(t, "@everyone", resp[0]["mention"])
|
||||
headers := resp[0]["headers"].(map[string]interface{})
|
||||
assert.Equal(t, "***REDACTED***", headers["Authorization"])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2174,6 +2174,11 @@ func (n *NotificationManager) sendResolvedWebhook(webhook WebhookConfig, alertLi
|
|||
customFields := convertWebhookCustomFields(webhook.CustomFields)
|
||||
data := n.prepareWebhookData(alert, customFields)
|
||||
|
||||
// Carry the configured mention into the resolved payload too, matching the
|
||||
// firing/grouped paths so a configured @everyone/@channel is not dropped on
|
||||
// resolved notifications (#1118).
|
||||
data.Mention = webhook.Mention
|
||||
|
||||
// Override fields for resolved context
|
||||
data.Event = "resolved"
|
||||
data.ResolvedAt = resolvedAt.Format(time.RFC3339)
|
||||
|
|
|
|||
|
|
@ -266,6 +266,90 @@ func TestSendResolvedWebhookHTTP(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSendResolvedWebhookIncludesMention(t *testing.T) {
|
||||
cases := []struct {
|
||||
service string
|
||||
assertFn func(t *testing.T, payload map[string]any)
|
||||
}{
|
||||
{
|
||||
service: "discord",
|
||||
assertFn: func(t *testing.T, payload map[string]any) {
|
||||
if payload["content"] != "@everyone" {
|
||||
t.Fatalf("expected discord resolved mention in content, got %v", payload["content"])
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "slack",
|
||||
assertFn: func(t *testing.T, payload map[string]any) {
|
||||
text, _ := payload["text"].(string)
|
||||
if !strings.HasPrefix(text, "@everyone ") {
|
||||
t.Fatalf("expected slack resolved text to start with mention, got %q", text)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "mattermost",
|
||||
assertFn: func(t *testing.T, payload map[string]any) {
|
||||
text, _ := payload["text"].(string)
|
||||
if !strings.HasPrefix(text, "@everyone\n\n") {
|
||||
t.Fatalf("expected mattermost resolved text to start with mention, got %q", text)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.service, func(t *testing.T) {
|
||||
var gotBody []byte
|
||||
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
gotBody = body
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := NewNotificationManager("")
|
||||
defer manager.Stop()
|
||||
manager.webhookClient = server.Client()
|
||||
if err := manager.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
||||
t.Fatalf("allowlist: %v", err)
|
||||
}
|
||||
|
||||
alertList := []*alerts.Alert{{
|
||||
ID: "a1",
|
||||
Type: "cpu",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "vm100",
|
||||
ResourceName: "web-server",
|
||||
Node: "pve1",
|
||||
Message: "CPU usage high",
|
||||
Value: 95,
|
||||
Threshold: 90,
|
||||
StartTime: time.Now().Add(-5 * time.Minute),
|
||||
}}
|
||||
|
||||
webhook := WebhookConfig{
|
||||
Name: tc.service + "-test",
|
||||
URL: server.URL + "/" + tc.service,
|
||||
Enabled: true,
|
||||
Service: tc.service,
|
||||
Mention: "@everyone",
|
||||
}
|
||||
|
||||
if err := manager.sendResolvedWebhook(webhook, alertList, time.Now()); err != nil {
|
||||
t.Fatalf("sendResolvedWebhook error: %v", err)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(gotBody, &payload); err != nil {
|
||||
t.Fatalf("unmarshal payload: %v (body=%s)", err, gotBody)
|
||||
}
|
||||
tc.assertFn(t, payload)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendResolvedWebhookNtfySingleAlert(t *testing.T) {
|
||||
var gotMethod string
|
||||
var gotBody string
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
}`,
|
||||
ResolvedPayloadTemplate: `{
|
||||
"username": "Pulse Monitoring",
|
||||
{{if .Mention}}"content": "{{.Mention | jsonString}}",{{end}}
|
||||
"embeds": [{
|
||||
"title": "Resolved: {{.ResourceName | jsonString}}",
|
||||
"description": "{{.Message | jsonString}}",
|
||||
|
|
@ -151,8 +152,15 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
]
|
||||
}`,
|
||||
ResolvedPayloadTemplate: `{
|
||||
"text": "Resolved: {{.ResourceName | jsonString}}",
|
||||
"text": "{{if .Mention}}{{.Mention | jsonString}} {{end}}Resolved: {{.ResourceName | jsonString}}",
|
||||
"blocks": [
|
||||
{{if .Mention}}{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "{{.Mention | jsonString}}"
|
||||
}
|
||||
},{{end}}
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
|
|
@ -235,6 +243,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
|||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": "2DC72D",
|
||||
"summary": "Resolved: {{.ResourceName | jsonString}}",
|
||||
{{if .Mention}}"text": "{{.Mention | jsonString}}",{{end}}
|
||||
"sections": [{
|
||||
"activityTitle": "Resolved: {{.ResourceName | jsonString}}",
|
||||
"activitySubtitle": "{{.Message | jsonString}}",
|
||||
|
|
@ -512,7 +521,7 @@ View in Pulse: {{.Instance}}`,
|
|||
ResolvedPayloadTemplate: `{
|
||||
"username": "Pulse Monitoring",
|
||||
"icon_url": "https://raw.githubusercontent.com/rcourtman/Pulse/main/frontend-modern/public/android-chrome-192x192.png",
|
||||
"text": ":white_check_mark: **RESOLVED**\n\n**{{.ResourceName | jsonString}}** on **{{.Node | jsonString}}**\n\n{{.Message | jsonString}}\n\n| Detail | Value |\n|:-------|:------|\n| Resource | {{.ResourceName | jsonString}} |\n| Node | {{.Node | jsonString}} |\n| Type | {{.Type | title | jsonString}} |\n| Duration | {{.Duration | jsonString}} |\n| Resolved At | {{.ResolvedAt | jsonString}} |\n| Alert Identifier | {{.ID | jsonString}} |"
|
||||
"text": "{{if .Mention}}{{.Mention | jsonString}}\n\n{{end}}:white_check_mark: **RESOLVED**\n\n**{{.ResourceName | jsonString}}** on **{{.Node | jsonString}}**\n\n{{.Message | jsonString}}\n\n| Detail | Value |\n|:-------|:------|\n| Resource | {{.ResourceName | jsonString}} |\n| Node | {{.Node | jsonString}} |\n| Type | {{.Type | title | jsonString}} |\n| Duration | {{.Duration | jsonString}} |\n| Resolved At | {{.ResolvedAt | jsonString}} |\n| Alert Identifier | {{.ID | jsonString}} |"
|
||||
}`,
|
||||
Instructions: "1. In Mattermost, go to Integrations > Incoming Webhooks\n2. Create a new webhook and select the channel\n3. Copy the webhook URL and paste it here\n\nNote: This template uses Markdown formatting which is fully supported by Mattermost.",
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue