Skip to content

Commit 0798f43

Browse files
author
Daniel Schmidt
committed
Extract Slack Notifier functionality into its own class
1 parent fde137f commit 0798f43

5 files changed

Lines changed: 148 additions & 74 deletions

File tree

lib/beekeeper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'beekeeper/monkeypatch/docker'
22
require 'beekeeper/logging'
3+
require 'beekeeper/slack_notifier'
34
require 'beekeeper/watcher'
45

56
module Beekeeper

lib/beekeeper/slack_notifier.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require 'slack'
2+
3+
module Beekeeper
4+
class SlackNotifier
5+
# Name of the environment variable containing the string value of
6+
# the Slack API/OAuth token.
7+
SLACK_TOKEN_ENV = 'SLACK_API_TOKEN'.freeze
8+
9+
# Name of the environment variable containing the path to file
10+
# containing the value of the Slack token. This is used only if
11+
# SLACK_TOKEN_ENV is empty.
12+
SLACK_TOKEN_FILE_ENV = 'SLACK_API_TOKEN_FILE'.freeze
13+
14+
# Default path to the file that should contain the Slack token, if
15+
# SLACK_TOKEN_ENV and SLACK_TOKEN_FILE_ENV are not set.
16+
SLACK_TOKEN_DEFAULT_FILE = '/run/secrets/SLACK_API_TOKEN'.freeze
17+
18+
attr_reader :client
19+
20+
def initialize(client = default_client)
21+
@client = client
22+
end
23+
24+
def notify_event(event:, recipient:)
25+
subject = event.service_name \
26+
? "Service \"#{event.service_name}\" exited #{event.exit_code}"
27+
: "Container '#{event.actor.id[..7]}' exited #{event.exit_code}"
28+
29+
@client.chat_postMessage(
30+
channel: recipient,
31+
as_user: true,
32+
blocks: [
33+
{ type: 'header', text: { type: 'plain_text', text: subject } },
34+
# Event Summary
35+
{ type: 'section', fields: [{
36+
'Host Name' => Docker.info['Name'],
37+
'Swarm Node' => event.swarm_node_id,
38+
'Service Name' => event.service_name,
39+
'Container ID' => event.actor_short_id,
40+
'Exit Code' => event.exit_code,
41+
'Image' => event.image_shortname,
42+
}.map { |h, t| { type: 'mrkdwn', text: "*#{h}*: #{t}" } }
43+
]},
44+
# Container Logs (if available)
45+
{ type: 'section', text: { type: 'mrkdwn', text: '*Logs:*' } },
46+
{ type: 'section', text: { type: 'plain_text', text: event.get_logs } },
47+
],
48+
)
49+
end
50+
51+
private
52+
53+
def default_client
54+
Slack::Web::Client.new(token: default_token).tap do |client|
55+
client.auth_test
56+
end
57+
end
58+
59+
def default_token
60+
ENV.fetch(SLACK_TOKEN_ENV) do
61+
File.read(ENV[SLACK_TOKEN_FILE_ENV] || SLACK_TOKEN_DEFAULT_FILE).chomp
62+
end
63+
end
64+
end
65+
end

lib/beekeeper/watcher.rb

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require 'beekeeper/logging'
22
require 'beekeeper/monkeypatch/docker'
3+
require 'beekeeper/slack_notifier'
34
require 'docker'
4-
require 'slack'
55

66
module Beekeeper
77
class Watcher
@@ -15,19 +15,6 @@ class Watcher
1515
# still be avoided, as there is a race condition in which events could slip by unnoticed.
1616
MAX_TIME_BETWEEN_EVENTS = ENV.fetch('READ_TIMEOUT', 0).to_i
1717

18-
# Name of the environment variable containing the string value of
19-
# the Slack API/OAuth token.
20-
SLACK_TOKEN_ENV = 'SLACK_API_TOKEN'.freeze
21-
22-
# Name of the environment variable containing the path to file
23-
# containing the value of the Slack token. This is used only if
24-
# SLACK_TOKEN_ENV is empty.
25-
SLACK_TOKEN_FILE_ENV = 'SLACK_API_TOKEN_FILE'.freeze
26-
27-
# Default path to the file that should contain the Slack token, if
28-
# SLACK_TOKEN_ENV and SLACK_TOKEN_FILE_ENV are not set.
29-
SLACK_TOKEN_DEFAULT_FILE = '/run/secrets/SLACK_API_TOKEN'.freeze
30-
3118
# Comma-separated list of Slack channels/users. If a container or
3219
# service fails and does not have its own custom watchers, these
3320
# are notified.
@@ -41,8 +28,10 @@ class Watcher
4128
# of watchers for the service.
4229
WATCHERS_LABEL = 'beekeeper.watchers'.freeze
4330

44-
def initialize(slack = nil)
45-
@slack = slack || default_slack_client
31+
attr_reader :slack
32+
33+
def initialize(slack = Beekeeper::SlackNotifier.new)
34+
@slack = slack
4635
end
4736

4837
def clean_watchlist(watchlist)
@@ -53,18 +42,6 @@ def clean_watchlist(watchlist)
5342
.uniq
5443
end
5544

56-
def default_slack_client
57-
Slack::Web::Client.new(token: default_slack_token).tap do |client|
58-
client.auth_test
59-
end
60-
end
61-
62-
def default_slack_token
63-
ENV.fetch(SLACK_TOKEN_ENV) do
64-
File.read(ENV[SLACK_TOKEN_FILE_ENV] || SLACK_TOKEN_DEFAULT_FILE).chomp
65-
end
66-
end
67-
6845
def default_watchers
6946
ENV.fetch(WATCHERS_ENV, WATCHERS_DEFAULT).split(',')
7047
end
@@ -103,30 +80,7 @@ def handle(event)
10380
end
10481

10582
def notify!(recipient, event)
106-
subject = event.service_name \
107-
? "Service \"#{event.service_name}\" exited #{event.exit_code}"
108-
: "Container '#{event.actor.id[..7]}' exited #{event.exit_code}"
109-
110-
@slack.chat_postMessage(
111-
channel: recipient,
112-
as_user: true,
113-
blocks: [
114-
{ type: 'header', text: { type: 'plain_text', text: subject } },
115-
# Event Summary
116-
{ type: 'section', fields: [{
117-
'Host Name' => Docker.info['Name'],
118-
'Swarm Node' => event.swarm_node_id,
119-
'Service Name' => event.service_name,
120-
'Container ID' => event.actor_short_id,
121-
'Exit Code' => event.exit_code,
122-
'Image' => event.image_shortname,
123-
}.map { |h, t| { type: 'mrkdwn', text: "*#{h}*: #{t}" } }
124-
]},
125-
# Container Logs (if available)
126-
{ type: 'section', text: { type: 'mrkdwn', text: '*Logs:*' } },
127-
{ type: 'section', text: { type: 'plain_text', text: event.get_logs } },
128-
],
129-
)
83+
slack.notify_event(event:, recipient:)
13084
end
13185

13286
def stream_options

spec/slack_notifier_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require 'beekeeper/slack_notifier'
2+
require 'docker'
3+
require 'slack'
4+
5+
describe Beekeeper::SlackNotifier do
6+
let(:client) { instance_double(Slack::Web::Client) }
7+
subject(:notifier) { Beekeeper::SlackNotifier.new(client) }
8+
9+
describe 'sending event notifications' do
10+
context 'for a failed container' do
11+
it 'sends a well-formed chat message' do
12+
event = new_event(exit_code: 11)
13+
recipient = '#devops-alerts'
14+
15+
expect(client)
16+
.to receive(:chat_postMessage)
17+
.with hash_including({
18+
channel: recipient,
19+
blocks: array_including(hash_including({
20+
type: 'header',
21+
text: {
22+
type: 'plain_text',
23+
text: "Container '12345' exited 11"
24+
}
25+
}))
26+
})
27+
28+
notifier.notify_event event:, recipient:
29+
end
30+
end
31+
end
32+
33+
def new_event(exit_code: 1, watchers: nil, service_name: nil)
34+
attrs = {
35+
'exitCode' => exit_code.to_s,
36+
'image' => 'containers.lib.berkeley.edu/lap/beekeeper:rspec-tests',
37+
}
38+
attrs['beekeeper.watchers'] = watchers if watchers
39+
attrs['com.docker.swarm.service.name'] = service_name if service_name
40+
41+
Docker::Event.new(
42+
{
43+
Action: 'die',
44+
Actor: {
45+
ID: '12345',
46+
Attributes: attrs,
47+
},
48+
}
49+
)
50+
end
51+
end

spec/watcher_spec.rb

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,58 @@
33
require 'slack'
44

55
describe Beekeeper::Watcher do
6-
subject { Beekeeper::Watcher.new(slack) }
6+
let(:notifier) { instance_double(Beekeeper::SlackNotifier) }
7+
subject(:watcher) { Beekeeper::Watcher.new(notifier) }
78

8-
let(:slack) { instance_double(Slack::Web::Client) }
9-
10-
describe '#watch!' do
11-
context 'a container without labels' do
9+
describe 'watching failure events' do
10+
context 'for an unlabeled container' do
1211
before { @old_watchers = ENV.delete('BEEKEEPER_WATCHERS') }
1312
after { ENV['BEEKEEPER_WATCHERS'] = @old_watchers }
1413

1514
it 'notifies default channels' do
16-
expect_docker_event new_event
17-
expect_slack_notifications %w(#devops-alerts)
15+
event = new_event
16+
recipients = %w(#devops-alerts)
17+
expect_docker_event event
18+
expect_event_notification(event:, recipients:)
1819

19-
subject.watch!
20+
watcher.watch!
2021
end
2122
end
2223

23-
context 'a container with labels' do
24+
context 'for a container with labels' do
2425
it 'notifies the correct channels' do
25-
expect_docker_event new_event(watchers: '@some-user,#some-channel')
26-
expect_slack_notifications %w(#some-channel @some-user)
26+
recipients = %w(#some-channel @some-user)
27+
event = new_event(watchers: recipients)
28+
expect_docker_event event
29+
expect_event_notification(event:, recipients:)
2730

28-
subject.watch!
31+
watcher.watch!
2932
end
3033
end
3134
end
3235

33-
def expect_slack_notifications(channels)
34-
channels.each do |channel|
35-
expect(slack)
36-
.to receive(:chat_postMessage)
37-
.once
38-
.ordered
39-
.with(hash_including(channel: channel))
40-
end
41-
end
36+
private
4237

4338
def expect_docker_event(event)
4439
expect(Docker::Event)
4540
.to receive(:stream)
4641
.and_yield(event)
4742
end
4843

44+
def expect_event_notification(event:, recipients:)
45+
recipients.each do |recipient|
46+
expect(notifier)
47+
.to receive(:notify_event)
48+
.with(event:, recipient:)
49+
end
50+
end
51+
4952
def new_event(exit_code: 1, watchers: nil, service_name: nil)
5053
attrs = {
5154
'exitCode' => exit_code.to_s,
5255
'image' => 'containers.lib.berkeley.edu/lap/beekeeper:rspec-tests',
5356
}
54-
attrs['beekeeper.watchers'] = watchers if watchers
57+
attrs['beekeeper.watchers'] = watchers.join(',') if watchers
5558
attrs['com.docker.swarm.service.name'] = service_name if service_name
5659

5760
Docker::Event.new(

0 commit comments

Comments
 (0)