From 95b8b38d6990fd021743eab56da96a01f9f97862 Mon Sep 17 00:00:00 2001 From: Lennart <3391295+lnsp@users.noreply.github.com> Date: Tue, 27 Sep 2022 19:57:16 +0200 Subject: [PATCH] Add support for Telegram (#10) * Add support for Telegram in chat package * Replace ioutil.ReadFile with os.ReadFile * Document use of Telegram as notification destination * Make Telegram message more compact * Simplify Dockerfile * Use master tag by default for images --- Dockerfile | 4 +- README.md | 17 +++++++ manifests/mattermost-informer.yaml | 2 +- manifests/slack-informer.yaml | 2 +- manifests/telegram-informer.yaml | 69 ++++++++++++++++++++++++++ pkg/chat/chat.go | 80 ++++++++++++++++++++++++++++++ pkg/utils/utils.go | 4 +- 7 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 manifests/telegram-informer.yaml diff --git a/Dockerfile b/Dockerfile index 3b20a60..46a766a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,10 @@ RUN apk update && \ mkdir -p "/build" WORKDIR /build -COPY go.mod go.sum /build/ +COPY go.mod go.sum . RUN go mod download -COPY . /build/ +COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -a --installsuffix cgo --ldflags="-s" -o informer FROM alpine diff --git a/README.md b/README.md index c618d87..f44ba95 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,20 @@ metadata: You should use the Bot User OAuth Access Token as `token`. It can be copied from the Slack App admin interface after registering a new Slack API App and enabling the Bot feature. +##### If you use Telegram, use + +```yaml +apiVersion: v1 +data: + chatId: + token: +kind: ConfigMap +metadata: + name: telegram-informer-cfg +``` + +Extracting the chat ID in Telegram can be slightly finicky. The easiest way I've found is using the ID exposed in the URLs when using the web.telegram.org frontend. + This step is required to create a valid configuration for our crash informer. ### Step 2: Deploy the informer @@ -43,6 +57,9 @@ kubectl apply -f manifests/mattermost-informer.yaml # If you use Slack kubectl apply -f manifests/slack-informer.yaml + +# If you use Telegram +kubectl apply -f manifests/telegram-informer.yaml ``` You may want to update the `namespace` references, since the informer only watches a given namespace. diff --git a/manifests/mattermost-informer.yaml b/manifests/mattermost-informer.yaml index 35af57a..7aec5da 100644 --- a/manifests/mattermost-informer.yaml +++ b/manifests/mattermost-informer.yaml @@ -48,7 +48,7 @@ spec: serviceAccountName: crash-informer containers: - name: informer - image: ghcr.io/lnsp/k8s-crash-informer:v0.2.1 + image: ghcr.io/lnsp/k8s-crash-informer:master imagePullPolicy: Always env: - name: MATTERMOST_CHANNEL diff --git a/manifests/slack-informer.yaml b/manifests/slack-informer.yaml index 84499c9..fc6e931 100644 --- a/manifests/slack-informer.yaml +++ b/manifests/slack-informer.yaml @@ -48,7 +48,7 @@ spec: serviceAccountName: crash-informer containers: - name: informer - image: ghcr.io/lnsp/k8s-crash-informer:v0.2.1 + image: ghcr.io/lnsp/k8s-crash-informer:master imagePullPolicy: Always env: - name: SLACK_CHANNEL diff --git a/manifests/telegram-informer.yaml b/manifests/telegram-informer.yaml new file mode 100644 index 0000000..d8e61e1 --- /dev/null +++ b/manifests/telegram-informer.yaml @@ -0,0 +1,69 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: crash-informer + namespace: default +rules: +- apiGroups: [""] + resources: ["pods", "pods/log", "replicationcontrollers"] + verbs: ["get", "watch", "list"] +- apiGroups: ["apps", "extensions"] + resources: ["replicasets", "deployments"] + verbs: ["get", "watch", "list"] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: crash-informer + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: crash-informer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: crash-informer +subjects: + - kind: ServiceAccount + name: crash-informer + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: telegram-informer + namespace: default +spec: + selector: + matchLabels: + app: telegram-informer + template: + metadata: + labels: + app: telegram-informer + spec: + restartPolicy: Always + serviceAccountName: crash-informer + containers: + - name: informer + image: ghcr.io/lnsp/k8s-crash-informer:master + imagePullPolicy: Always + env: + - name: TELEGRAM_CHATID + valueFrom: + configMapKeyRef: + name: telegram-informer-cfg + key: chatId + - name: TELEGRAM_TOKEN + valueFrom: + configMapKeyRef: + name: telegram-informer-cfg + key: token + - name: INFORMER_TYPE + value: telegram + resources: + limits: + memory: "128Mi" + cpu: "100m" diff --git a/pkg/chat/chat.go b/pkg/chat/chat.go index ec1805e..00e721b 100644 --- a/pkg/chat/chat.go +++ b/pkg/chat/chat.go @@ -2,7 +2,10 @@ package chat import ( "fmt" + "strings" + "unicode/utf16" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/kelseyhightower/envconfig" "github.com/mattermost/mattermost-server/v6/model" "github.com/slack-go/slack" @@ -84,6 +87,8 @@ func NewClientFromEnv() (Client, error) { client, err = NewMattermostClientFromEnv() case "slack": client, err = NewSlackClientFromEnv() + case "telegram": + client, err = NewTelegramClientFromEnv() default: err = fmt.Errorf("unknown client type: %s", cfg.Type) } @@ -163,3 +168,78 @@ func NewSlackClientFromEnv() (*SlackClient, error) { Channel: cfg.Channel, }, nil } + +type TelegramConfig struct { + Token string + ChatID int64 +} + +type TelegramClient struct { + chat tgbotapi.Chat + bot *tgbotapi.BotAPI +} + +func (client *TelegramClient) Send(note *CrashNotification) { + // Generate text + type entityMarker struct { + Type string + Content string + } + contents := []entityMarker{ + {"bold", note.Title}, + {"", note.Message}, + {"bold", "Logs"}, + {"pre", note.Logs}, + {"bold", "Reason"}, + {"pre", note.Reason}, + } + // Generate text + cleartext := []string{} + for _, c := range contents { + cleartext = append(cleartext, c.Content) + } + // Get all joined groups + message := tgbotapi.NewMessage(client.chat.ID, strings.Join(cleartext, "\n")) + offset := 0 + // Generate list of entities + for _, c := range contents { + length := len(utf16.Encode([]rune(c.Content))) + if c.Type != "" { + message.Entities = append(message.Entities, tgbotapi.MessageEntity{ + Type: c.Type, + Offset: offset, + Length: length, + }) + } + offset += length + 1 + } + _, err := client.bot.Send(message) + if err != nil { + fmt.Println(err) + } +} + +// NewTelegramClientFromEnv instantiates and configures a Telegram client. +func NewTelegramClientFromEnv() (*TelegramClient, error) { + var cfg TelegramConfig + if err := envconfig.Process("telegram", &cfg); err != nil { + return nil, err + } + bot, err := tgbotapi.NewBotAPI(cfg.Token) + if err != nil { + return nil, fmt.Errorf("init bot: %w", err) + } + // Get chat to verify + chat, err := bot.GetChat(tgbotapi.ChatInfoConfig{ + ChatConfig: tgbotapi.ChatConfig{ + ChatID: cfg.ChatID, + }, + }) + if err != nil { + return nil, fmt.Errorf("get chat: %w", err) + } + return &TelegramClient{ + chat: chat, + bot: bot, + }, nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index fb26793..01d7a61 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,14 +2,14 @@ package utils import ( "fmt" - "io/ioutil" + "os" ) const namespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" // Namespace returns the namespace this pod is running in. func Namespace() (string, error) { - nsfile, err := ioutil.ReadFile(namespaceFilePath) + nsfile, err := os.ReadFile(namespaceFilePath) if err != nil { return "", fmt.Errorf("could not read namespace: %v", err) }