Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions rocketpool-cli/service/config/settings-alerting.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ var alertingParametersNativeMode = map[string]interface{}{
"nativeModeHost": nil,
"nativeModePort": nil,
"discordWebhookURL": nil,
"telegramBotToken": nil,
"telegramChatId": nil,
"pushoverToken": nil,
"pushoverUserKey": nil,
"alertEnabled_FeeRecipientChanged": nil,
Expand All @@ -36,6 +38,8 @@ var alertingParametersDockerMode = map[string]interface{}{
"openPort": nil,
"containerTag": nil,
"discordWebhookURL": nil,
"telegramBotToken": nil,
"telegramChatId": nil,
"pushoverToken": nil,
"pushoverUserKey": nil,
"alertEnabled_ClientSyncStatusBeacon": nil,
Expand Down
43 changes: 43 additions & 0 deletions shared/services/config/alertmanager-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package config

import (
"fmt"
"strconv"
"strings"

"github.com/mitchellh/go-homedir"
"golang.org/x/text/cases"
Expand Down Expand Up @@ -57,6 +59,11 @@ type AlertmanagerConfig struct {
// The Discord webhook URL for alert notifications
DiscordWebhookURL config.Parameter `yaml:"discordWebhookURL,omitempty"`

// The Telegram bot token for alert notifications
TelegramBotToken config.Parameter `yaml:"telegramBotToken,omitempty"`
// The Telegram chat ID for alert notifications
TelegramChatID config.Parameter `yaml:"telegramChatId,omitempty"`

// The Pushover Token for alert notifications
PushoverToken config.Parameter `yaml:"pushoverToken,omitempty"`
// The Pushover User Key for alert notifications
Expand Down Expand Up @@ -185,6 +192,28 @@ func NewAlertmanagerConfig(cfg *RocketPoolConfig) *AlertmanagerConfig {
OverwriteOnUpgrade: false,
},

TelegramBotToken: config.Parameter{
ID: "telegramBotToken",
Name: "Alertmanager Telegram Bot Token",
Description: "Telegram notifications are sent via the Telegram Bot API. Create a bot with @BotFather (https://t.me/BotFather) and paste the token here (the token only — do not include a 'bot' prefix). Both this token and a chat ID are required; filling only one does nothing.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: ""},
AffectsContainers: []config.ContainerID{config.ContainerID_Alertmanager},
CanBeBlank: true,
OverwriteOnUpgrade: false,
},

TelegramChatID: config.Parameter{
ID: "telegramChatId",
Name: "Alertmanager Telegram Chat ID",
Description: "Numeric Telegram chat ID to send alerts to (user, group, or channel). Group and channel IDs are negative (typically -100…). For DMs, send /start to your bot first. For groups or channels, add the bot as a member (admin for channels). Both this chat ID and a bot token are required; filling only one does nothing.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: ""},
AffectsContainers: []config.ContainerID{config.ContainerID_Alertmanager},
CanBeBlank: true,
OverwriteOnUpgrade: false,
},

PushoverToken: config.Parameter{
ID: "pushoverToken",
Name: "Alertmanager Pushover Token",
Expand Down Expand Up @@ -328,6 +357,8 @@ func (cfg *AlertmanagerConfig) GetParameters() []*config.Parameter {
&cfg.NativeModeHost,
&cfg.NativeModePort,
&cfg.DiscordWebhookURL,
&cfg.TelegramBotToken,
&cfg.TelegramChatID,
&cfg.PushoverToken,
&cfg.PushoverUserKey,
&cfg.ContainerTag,
Expand Down Expand Up @@ -359,6 +390,18 @@ func (cfg *AlertmanagerConfig) GetConfigTitle() string {
return cfg.Title
}

// TelegramEnabled reports whether Telegram notifications should be rendered into
// alertmanager.yml. Both a bot token and a numeric chat ID are required.
func (cfg *AlertmanagerConfig) TelegramEnabled() bool {
token, _ := cfg.TelegramBotToken.Value.(string)
chat, _ := cfg.TelegramChatID.Value.(string)
if strings.TrimSpace(token) == "" || strings.TrimSpace(chat) == "" {
return false
}
_, err := strconv.ParseInt(strings.TrimSpace(chat), 10, 64)
return err == nil
}

// Used by text/template to format alertmanager.yml
func (cfg *AlertmanagerConfig) GetOpenPorts() string {
portMode := cfg.OpenPort.Value.(config.RPCMode)
Expand Down
90 changes: 90 additions & 0 deletions shared/services/config/alertmanager-config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package config

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"text/template"
)

func TestTelegramEnabled(t *testing.T) {
tests := []struct {
name string
token string
chat string
want bool
}{
{name: "both empty", want: false},
{name: "empty token", token: "", chat: "12345", want: false},
{name: "empty chat", token: "123:ABC", chat: "", want: false},
{name: "whitespace only", token: " ", chat: " ", want: false},
{name: "non-numeric chat", token: "123:ABC", chat: "not-a-chat-id", want: false},
{name: "username chat", token: "123:ABC", chat: "@mychannel", want: false},
{name: "valid positive chat", token: "123:ABC", chat: "12345", want: true},
{name: "valid negative group chat", token: "123:ABC", chat: "-1001234567890", want: true},
{name: "padded numeric chat", token: " 123:ABC ", chat: " 12345 ", want: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &AlertmanagerConfig{}
cfg.TelegramBotToken.Value = tt.token
cfg.TelegramChatID.Value = tt.chat
if got := cfg.TelegramEnabled(); got != tt.want {
t.Fatalf("TelegramEnabled() = %v, want %v (token=%q chat=%q)", got, tt.want, tt.token, tt.chat)
}
})
}
}

func TestAlertmanagerTemplateTelegramConfigs(t *testing.T) {
tmplPath := filepath.Join("..", "rocketpool", "assets", "install", "alerting", "alertmanager.tmpl")
src, err := os.ReadFile(tmplPath)
if err != nil {
t.Fatalf("read alertmanager template: %v", err)
}

tmpl, err := template.New("alertmanager.tmpl").Parse(string(src))
if err != nil {
t.Fatalf("parse alertmanager template: %v", err)
}

cfg := &AlertmanagerConfig{}
cfg.TelegramBotToken.Value = "123:ABC"
cfg.TelegramChatID.Value = "-1001234567890"

var buf bytes.Buffer
if err := tmpl.Execute(&buf, cfg); err != nil {
t.Fatalf("execute alertmanager template: %v", err)
}
out := buf.String()

if !strings.Contains(out, "telegram_configs:") {
t.Fatalf("expected telegram_configs when Telegram is enabled:\n%s", out)
}
if !strings.Contains(out, `bot_token: "123:ABC"`) {
t.Fatalf("expected quoted bot token:\n%s", out)
}
if !strings.Contains(out, "chat_id: -1001234567890") {
t.Fatalf("expected unquoted chat ID:\n%s", out)
}
if strings.Count(out, "telegram_configs:") != 2 {
t.Fatalf("expected telegram_configs on both receivers, found %d:\n%s", strings.Count(out, "telegram_configs:"), out)
}
if strings.Count(out, "send_resolved: false") < 1 {
t.Fatalf("expected send_resolved: false on the info receiver:\n%s", out)
}

disabled := &AlertmanagerConfig{}
disabled.TelegramBotToken.Value = "123:ABC"
disabled.TelegramChatID.Value = "not-a-chat-id"
buf.Reset()
if err := tmpl.Execute(&buf, disabled); err != nil {
t.Fatalf("execute alertmanager template with invalid chat ID: %v", err)
}
if strings.Contains(buf.String(), "telegram_configs:") {
t.Fatalf("did not expect telegram_configs for invalid chat ID:\n%s", buf.String())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ receivers:
- webhook_url: "{{ .DiscordWebhookURL.Value }}"
{{- end }}

{{- if .TelegramEnabled }}
telegram_configs:
- bot_token: "{{ .TelegramBotToken.Value }}"
chat_id: {{ .TelegramChatID.Value }}
{{- end }}

{{- if and .PushoverToken.Value .PushoverUserKey.Value }}
pushover_configs:
- token: "{{ .PushoverToken.Value }}"
Expand All @@ -54,6 +60,13 @@ receivers:
- webhook_url: "{{ .DiscordWebhookURL.Value }}"
send_resolved: false
{{- end }}

{{- if .TelegramEnabled }}
telegram_configs:
- bot_token: "{{ .TelegramBotToken.Value }}"
chat_id: {{ .TelegramChatID.Value }}
send_resolved: false
{{- end }}

{{- if and .PushoverToken.Value .PushoverUserKey.Value }}
pushover_configs:
Expand Down
Loading