From 6c9c58015e051cda5fe8f48e6918a4c7646d5538 Mon Sep 17 00:00:00 2001 From: Andrey Mishakin Date: Sun, 22 Jan 2023 00:43:34 +0300 Subject: [PATCH] Support loading Telegram bot token from file Signed-off-by: Andrey Mishakin --- config/notifiers.go | 8 +++- config/notifiers_test.go | 64 ++++++++++++++++++++++++++++++++ docs/configuration.md | 5 ++- notify/telegram/telegram.go | 24 ++++++++++-- notify/telegram/telegram_test.go | 20 ++++++++++ 5 files changed, 114 insertions(+), 7 deletions(-) diff --git a/config/notifiers.go b/config/notifiers.go index d84fa184..82adb4df 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -739,6 +739,7 @@ type TelegramConfig struct { APIUrl *URL `yaml:"api_url" json:"api_url,omitempty"` BotToken Secret `yaml:"bot_token,omitempty" json:"token,omitempty"` + BotTokenFile string `yaml:"bot_token_file,omitempty" json:"token_file,omitempty"` ChatID int64 `yaml:"chat_id,omitempty" json:"chat,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` DisableNotifications bool `yaml:"disable_notifications,omitempty" json:"disable_notifications,omitempty"` @@ -752,8 +753,11 @@ func (c *TelegramConfig) UnmarshalYAML(unmarshal func(interface{}) error) error if err := unmarshal((*plain)(c)); err != nil { return err } - if c.BotToken == "" { - return fmt.Errorf("missing bot_token on telegram_config") + if c.BotToken == "" && c.BotTokenFile == "" { + return fmt.Errorf("missing bot_token or bot_token_file on telegram_config") + } + if c.BotToken != "" && c.BotTokenFile != "" { + return fmt.Errorf("at most one of bot_token & bot_token_file must be configured") } if c.ChatID == 0 { return fmt.Errorf("missing chat_id on telegram_config") diff --git a/config/notifiers_test.go b/config/notifiers_test.go index a1947832..89af5628 100644 --- a/config/notifiers_test.go +++ b/config/notifiers_test.go @@ -958,6 +958,70 @@ http_config: } } +func TestTelegramConfiguration(t *testing.T) { + tc := []struct { + name string + in string + expected error + }{ + { + name: "with both bot_token & bot_token_file - it fails", + in: ` +bot_token: xyz +bot_token_file: /file +`, + expected: errors.New("at most one of bot_token & bot_token_file must be configured"), + }, + { + name: "with no bot_token & bot_token_file - it fails", + in: ` +bot_token: '' +bot_token_file: '' +`, + expected: errors.New("missing bot_token or bot_token_file on telegram_config"), + }, + { + name: "with bot_token and chat_id set - it succeeds", + in: ` +bot_token: xyz +chat_id: 123 +`, + }, + { + name: "with bot_token_file and chat_id set - it succeeds", + in: ` +bot_token_file: /file +chat_id: 123 +`, + }, + { + name: "with no chat_id set - it fails", + in: ` +bot_token: xyz +`, + expected: errors.New("missing chat_id on telegram_config"), + }, + { + name: "with unknown parse_mode - it fails", + in: ` +bot_token: xyz +chat_id: 123 +parse_mode: invalid +`, + expected: errors.New("unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string"), + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + var cfg TelegramConfig + err := yaml.UnmarshalStrict([]byte(tt.in), &cfg) + + require.Equal(t, tt.expected, err) + }) + } +} + func newBoolPointer(b bool) *bool { return &b } diff --git a/docs/configuration.md b/docs/configuration.md index e128afad..f16fe001 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1050,9 +1050,12 @@ attributes: # If not specified, default API URL will be used. [ api_url: | default = global.telegram_api_url ] -# Telegram bot token. +# Telegram bot token. It is mutually exclusive with `bot_token_file`. [ bot_token: ] +# Read the Telegram bot token from a file. It is mutually exclusive with `bot_token`. +[ bot_token_file: ] + # ID of the chat where to send the messages. [ chat_id: ] diff --git a/notify/telegram/telegram.go b/notify/telegram/telegram.go index 64f6ce80..3e60ab76 100644 --- a/notify/telegram/telegram.go +++ b/notify/telegram/telegram.go @@ -17,6 +17,8 @@ import ( "context" "fmt" "net/http" + "os" + "strings" "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -48,7 +50,7 @@ func New(conf *config.TelegramConfig, t *template.Template, l log.Logger, httpOp return nil, err } - client, err := createTelegramClient(conf.BotToken, conf.APIUrl.String(), conf.ParseMode, httpclient) + client, err := createTelegramClient(conf.APIUrl.String(), conf.ParseMode, httpclient) if err != nil { return nil, err } @@ -83,6 +85,11 @@ func (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, err level.Warn(n.logger).Log("msg", "Truncated message", "alert", key, "max_runes", maxMessageLenRunes) } + n.client.Token, err = n.getBotToken() + if err != nil { + return true, err + } + message, err := n.client.Send(telebot.ChatID(n.conf.ChatID), messageText, &telebot.SendOptions{ DisableNotification: n.conf.DisableNotifications, DisableWebPagePreview: true, @@ -95,10 +102,8 @@ func (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, err return false, nil } -func createTelegramClient(token config.Secret, apiURL, parseMode string, httpClient *http.Client) (*telebot.Bot, error) { - secret := string(token) +func createTelegramClient(apiURL, parseMode string, httpClient *http.Client) (*telebot.Bot, error) { bot, err := telebot.NewBot(telebot.Settings{ - Token: secret, URL: apiURL, ParseMode: parseMode, Client: httpClient, @@ -110,3 +115,14 @@ func createTelegramClient(token config.Secret, apiURL, parseMode string, httpCli return bot, nil } + +func (n *Notifier) getBotToken() (string, error) { + if len(n.conf.BotTokenFile) > 0 { + content, err := os.ReadFile(n.conf.BotTokenFile) + if err != nil { + return "", fmt.Errorf("could not read %s: %w", n.conf.BotTokenFile, err) + } + return strings.TrimSpace(string(content)), nil + } + return string(n.conf.BotToken), nil +} diff --git a/notify/telegram/telegram_test.go b/notify/telegram/telegram_test.go index c7a8f333..c48034ca 100644 --- a/notify/telegram/telegram_test.go +++ b/notify/telegram/telegram_test.go @@ -21,6 +21,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "testing" "time" @@ -84,6 +85,13 @@ func TestTelegramRetry(t *testing.T) { } func TestTelegramNotify(t *testing.T) { + token := "secret" + + fileWithToken, err := os.CreateTemp("", "telegram-bot-token") + require.NoError(t, err, "creating temp file failed") + _, err = fileWithToken.WriteString(token) + require.NoError(t, err, "writing to temp file failed") + for _, tc := range []struct { name string cfg config.TelegramConfig @@ -94,6 +102,7 @@ func TestTelegramNotify(t *testing.T) { cfg: config.TelegramConfig{ Message: "x < y", HTTPConfig: &commoncfg.HTTPClientConfig{}, + BotToken: config.Secret(token), }, expText: "x < y", }, @@ -103,13 +112,24 @@ func TestTelegramNotify(t *testing.T) { ParseMode: "HTML", Message: "x < y", HTTPConfig: &commoncfg.HTTPClientConfig{}, + BotToken: config.Secret(token), }, expText: "x < y", }, + { + name: "Bot token from file", + cfg: config.TelegramConfig{ + Message: "test", + HTTPConfig: &commoncfg.HTTPClientConfig{}, + BotTokenFile: fileWithToken.Name(), + }, + expText: "test", + }, } { t.Run(tc.name, func(t *testing.T) { var out []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/bot"+token+"/sendMessage", r.URL.Path) var err error out, err = io.ReadAll(r.Body) require.NoError(t, err)