Skip to content

Commit 95b8b38

Browse files
authored
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
1 parent 5948e47 commit 95b8b38

File tree

7 files changed

+172
-6
lines changed

7 files changed

+172
-6
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ RUN apk update && \
88
mkdir -p "/build"
99

1010
WORKDIR /build
11-
COPY go.mod go.sum /build/
11+
COPY go.mod go.sum .
1212
RUN go mod download
1313

14-
COPY . /build/
14+
COPY . .
1515
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -a --installsuffix cgo --ldflags="-s" -o informer
1616

1717
FROM alpine

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ metadata:
3434
3535
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.
3636

37+
##### If you use Telegram, use
38+
39+
```yaml
40+
apiVersion: v1
41+
data:
42+
chatId: <chat-id>
43+
token: <bot-token>
44+
kind: ConfigMap
45+
metadata:
46+
name: telegram-informer-cfg
47+
```
48+
49+
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.
50+
3751
This step is required to create a valid configuration for our crash informer.
3852

3953
### Step 2: Deploy the informer
@@ -43,6 +57,9 @@ kubectl apply -f manifests/mattermost-informer.yaml
4357
4458
# If you use Slack
4559
kubectl apply -f manifests/slack-informer.yaml
60+
61+
# If you use Telegram
62+
kubectl apply -f manifests/telegram-informer.yaml
4663
```
4764

4865
You may want to update the `namespace` references, since the informer only watches a given namespace.

manifests/mattermost-informer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ spec:
4848
serviceAccountName: crash-informer
4949
containers:
5050
- name: informer
51-
image: ghcr.io/lnsp/k8s-crash-informer:v0.2.1
51+
image: ghcr.io/lnsp/k8s-crash-informer:master
5252
imagePullPolicy: Always
5353
env:
5454
- name: MATTERMOST_CHANNEL

manifests/slack-informer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ spec:
4848
serviceAccountName: crash-informer
4949
containers:
5050
- name: informer
51-
image: ghcr.io/lnsp/k8s-crash-informer:v0.2.1
51+
image: ghcr.io/lnsp/k8s-crash-informer:master
5252
imagePullPolicy: Always
5353
env:
5454
- name: SLACK_CHANNEL

manifests/telegram-informer.yaml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
kind: Role
2+
apiVersion: rbac.authorization.k8s.io/v1
3+
metadata:
4+
name: crash-informer
5+
namespace: default
6+
rules:
7+
- apiGroups: [""]
8+
resources: ["pods", "pods/log", "replicationcontrollers"]
9+
verbs: ["get", "watch", "list"]
10+
- apiGroups: ["apps", "extensions"]
11+
resources: ["replicasets", "deployments"]
12+
verbs: ["get", "watch", "list"]
13+
---
14+
apiVersion: v1
15+
kind: ServiceAccount
16+
metadata:
17+
name: crash-informer
18+
namespace: default
19+
---
20+
apiVersion: rbac.authorization.k8s.io/v1
21+
kind: RoleBinding
22+
metadata:
23+
name: crash-informer
24+
roleRef:
25+
apiGroup: rbac.authorization.k8s.io
26+
kind: Role
27+
name: crash-informer
28+
subjects:
29+
- kind: ServiceAccount
30+
name: crash-informer
31+
namespace: default
32+
---
33+
apiVersion: apps/v1
34+
kind: Deployment
35+
metadata:
36+
name: telegram-informer
37+
namespace: default
38+
spec:
39+
selector:
40+
matchLabels:
41+
app: telegram-informer
42+
template:
43+
metadata:
44+
labels:
45+
app: telegram-informer
46+
spec:
47+
restartPolicy: Always
48+
serviceAccountName: crash-informer
49+
containers:
50+
- name: informer
51+
image: ghcr.io/lnsp/k8s-crash-informer:master
52+
imagePullPolicy: Always
53+
env:
54+
- name: TELEGRAM_CHATID
55+
valueFrom:
56+
configMapKeyRef:
57+
name: telegram-informer-cfg
58+
key: chatId
59+
- name: TELEGRAM_TOKEN
60+
valueFrom:
61+
configMapKeyRef:
62+
name: telegram-informer-cfg
63+
key: token
64+
- name: INFORMER_TYPE
65+
value: telegram
66+
resources:
67+
limits:
68+
memory: "128Mi"
69+
cpu: "100m"

pkg/chat/chat.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package chat
22

33
import (
44
"fmt"
5+
"strings"
6+
"unicode/utf16"
57

8+
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
69
"github.com/kelseyhightower/envconfig"
710
"github.com/mattermost/mattermost-server/v6/model"
811
"github.com/slack-go/slack"
@@ -84,6 +87,8 @@ func NewClientFromEnv() (Client, error) {
8487
client, err = NewMattermostClientFromEnv()
8588
case "slack":
8689
client, err = NewSlackClientFromEnv()
90+
case "telegram":
91+
client, err = NewTelegramClientFromEnv()
8792
default:
8893
err = fmt.Errorf("unknown client type: %s", cfg.Type)
8994
}
@@ -163,3 +168,78 @@ func NewSlackClientFromEnv() (*SlackClient, error) {
163168
Channel: cfg.Channel,
164169
}, nil
165170
}
171+
172+
type TelegramConfig struct {
173+
Token string
174+
ChatID int64
175+
}
176+
177+
type TelegramClient struct {
178+
chat tgbotapi.Chat
179+
bot *tgbotapi.BotAPI
180+
}
181+
182+
func (client *TelegramClient) Send(note *CrashNotification) {
183+
// Generate text
184+
type entityMarker struct {
185+
Type string
186+
Content string
187+
}
188+
contents := []entityMarker{
189+
{"bold", note.Title},
190+
{"", note.Message},
191+
{"bold", "Logs"},
192+
{"pre", note.Logs},
193+
{"bold", "Reason"},
194+
{"pre", note.Reason},
195+
}
196+
// Generate text
197+
cleartext := []string{}
198+
for _, c := range contents {
199+
cleartext = append(cleartext, c.Content)
200+
}
201+
// Get all joined groups
202+
message := tgbotapi.NewMessage(client.chat.ID, strings.Join(cleartext, "\n"))
203+
offset := 0
204+
// Generate list of entities
205+
for _, c := range contents {
206+
length := len(utf16.Encode([]rune(c.Content)))
207+
if c.Type != "" {
208+
message.Entities = append(message.Entities, tgbotapi.MessageEntity{
209+
Type: c.Type,
210+
Offset: offset,
211+
Length: length,
212+
})
213+
}
214+
offset += length + 1
215+
}
216+
_, err := client.bot.Send(message)
217+
if err != nil {
218+
fmt.Println(err)
219+
}
220+
}
221+
222+
// NewTelegramClientFromEnv instantiates and configures a Telegram client.
223+
func NewTelegramClientFromEnv() (*TelegramClient, error) {
224+
var cfg TelegramConfig
225+
if err := envconfig.Process("telegram", &cfg); err != nil {
226+
return nil, err
227+
}
228+
bot, err := tgbotapi.NewBotAPI(cfg.Token)
229+
if err != nil {
230+
return nil, fmt.Errorf("init bot: %w", err)
231+
}
232+
// Get chat to verify
233+
chat, err := bot.GetChat(tgbotapi.ChatInfoConfig{
234+
ChatConfig: tgbotapi.ChatConfig{
235+
ChatID: cfg.ChatID,
236+
},
237+
})
238+
if err != nil {
239+
return nil, fmt.Errorf("get chat: %w", err)
240+
}
241+
return &TelegramClient{
242+
chat: chat,
243+
bot: bot,
244+
}, nil
245+
}

pkg/utils/utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package utils
22

33
import (
44
"fmt"
5-
"io/ioutil"
5+
"os"
66
)
77

88
const namespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
99

1010
// Namespace returns the namespace this pod is running in.
1111
func Namespace() (string, error) {
12-
nsfile, err := ioutil.ReadFile(namespaceFilePath)
12+
nsfile, err := os.ReadFile(namespaceFilePath)
1313
if err != nil {
1414
return "", fmt.Errorf("could not read namespace: %v", err)
1515
}

0 commit comments

Comments
 (0)