Skip to content

Commit 81597fa

Browse files
authored
feat(slack): adding frequency options and test passed/failed notifications (#1808)
* feat(slack): adding frequncy options and test passed/failed notifications * fix(comments): add more comments * fix(nit): fixing nits
1 parent fe1f542 commit 81597fa

20 files changed

+412
-158
lines changed

keel-core/src/main/kotlin/com/netflix/spinnaker/keel/events/NotificationEvent.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.netflix.spinnaker.keel.notifications.Notification
99
import com.netflix.spinnaker.keel.notifications.NotificationScope
1010
import com.netflix.spinnaker.keel.notifications.NotificationType
1111

12-
abstract class NotificationEvent{
12+
abstract class NotificationEvent {
1313
abstract val scope: NotificationScope
1414
abstract val type: NotificationType
1515
}
@@ -25,15 +25,15 @@ abstract class RepeatedNotificationEvent {
2525
data class UnhealthyNotification(
2626
override val ref: String,
2727
override val message: Notification
28-
) :RepeatedNotificationEvent() {
29-
override val type = NotificationType.RESOURCE_UNHEALTHY
30-
override val scope = NotificationScope.RESOURCE
31-
}
28+
) : RepeatedNotificationEvent() {
29+
override val type = NotificationType.RESOURCE_UNHEALTHY
30+
override val scope = NotificationScope.RESOURCE
31+
}
3232

3333
data class PinnedNotification(
3434
val config: DeliveryConfig,
3535
val pin: EnvironmentArtifactPin
36-
): NotificationEvent() {
36+
) : NotificationEvent() {
3737
override val type = NotificationType.ARTIFACT_PINNED
3838
override val scope = NotificationScope.ARTIFACT
3939
}
@@ -43,7 +43,7 @@ data class UnpinnedNotification(
4343
val pinnedEnvironment: PinnedEnvironment?,
4444
val targetEnvironment: String,
4545
val user: String
46-
): NotificationEvent() {
46+
) : NotificationEvent() {
4747
override val type = NotificationType.ARTIFACT_UNPINNED
4848
override val scope = NotificationScope.ARTIFACT
4949
}
@@ -52,7 +52,7 @@ data class MarkAsBadNotification(
5252
val config: DeliveryConfig,
5353
val user: String,
5454
val veto: EnvironmentArtifactVeto
55-
): NotificationEvent() {
55+
) : NotificationEvent() {
5656
override val type = NotificationType.ARTIFACT_MARK_AS_BAD
5757
override val scope = NotificationScope.ARTIFACT
5858
}
@@ -62,7 +62,7 @@ data class ArtifactDeployedNotification(
6262
val artifactVersion: String,
6363
val deliveryArtifact: DeliveryArtifact,
6464
val targetEnvironment: String
65-
): NotificationEvent() {
66-
override val type = NotificationType.ARTIFACT_DEPLOYMENT
65+
) : NotificationEvent() {
66+
override val type = NotificationType.ARTIFACT_DEPLOYMENT_SUCCEDEED
6767
override val scope = NotificationScope.ARTIFACT
6868
}

keel-core/src/main/kotlin/com/netflix/spinnaker/keel/notifications/NotificationType.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ enum class NotificationType {
88
ARTIFACT_PINNED,
99
ARTIFACT_UNPINNED,
1010
ARTIFACT_MARK_AS_BAD,
11-
ARTIFACT_DEPLOYMENT,
11+
ARTIFACT_DEPLOYMENT_FAILED,
12+
ARTIFACT_DEPLOYMENT_SUCCEDEED,
1213
APPLICATION_PAUSED,
1314
APPLICATION_RESUMED,
1415
LIFECYCLE_EVENT,
15-
MANUAL_JUDGMENT
16+
MANUAL_JUDGMENT_AWAIT,
17+
MANUAL_JUDGMENT_REJECTED,
18+
MANUAL_JUDGMENT_APPROVED,
19+
TEST_PASSED,
20+
TEST_FAILED,
21+
DELIVEY_CONFIG_UPDATED
1622
}

keel-slack/src/main/kotlin/com/netflix/spinnaker/keel/slack/NotificationEventListener.kt

+90-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package com.netflix.spinnaker.keel.slack
22

33
import com.netflix.spinnaker.keel.api.DeliveryConfig
44
import com.netflix.spinnaker.keel.api.Environment
5+
import com.netflix.spinnaker.keel.api.NotificationFrequency
6+
import com.netflix.spinnaker.keel.api.NotificationFrequency.normal
7+
import com.netflix.spinnaker.keel.api.NotificationFrequency.quiet
8+
import com.netflix.spinnaker.keel.api.NotificationFrequency.verbose
59
import com.netflix.spinnaker.keel.api.NotificationType
610
import com.netflix.spinnaker.keel.api.artifacts.DeliveryArtifact
711
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus
@@ -16,10 +20,25 @@ import com.netflix.spinnaker.keel.events.PinnedNotification
1620
import com.netflix.spinnaker.keel.events.UnpinnedNotification
1721
import com.netflix.spinnaker.keel.lifecycle.LifecycleEvent
1822
import com.netflix.spinnaker.keel.lifecycle.LifecycleEventStatus
23+
import com.netflix.spinnaker.keel.notifications.NotificationType.APPLICATION_PAUSED
24+
import com.netflix.spinnaker.keel.notifications.NotificationType.APPLICATION_RESUMED
25+
import com.netflix.spinnaker.keel.notifications.NotificationType.ARTIFACT_DEPLOYMENT_FAILED
26+
import com.netflix.spinnaker.keel.notifications.NotificationType.ARTIFACT_DEPLOYMENT_SUCCEDEED
27+
import com.netflix.spinnaker.keel.notifications.NotificationType.ARTIFACT_MARK_AS_BAD
28+
import com.netflix.spinnaker.keel.notifications.NotificationType.ARTIFACT_PINNED
29+
import com.netflix.spinnaker.keel.notifications.NotificationType.ARTIFACT_UNPINNED
30+
import com.netflix.spinnaker.keel.notifications.NotificationType.DELIVEY_CONFIG_UPDATED
31+
import com.netflix.spinnaker.keel.notifications.NotificationType.LIFECYCLE_EVENT
32+
import com.netflix.spinnaker.keel.notifications.NotificationType.MANUAL_JUDGMENT_APPROVED
33+
import com.netflix.spinnaker.keel.notifications.NotificationType.MANUAL_JUDGMENT_AWAIT
34+
import com.netflix.spinnaker.keel.notifications.NotificationType.MANUAL_JUDGMENT_REJECTED
35+
import com.netflix.spinnaker.keel.notifications.NotificationType.TEST_FAILED
36+
import com.netflix.spinnaker.keel.notifications.NotificationType.TEST_PASSED
1937
import com.netflix.spinnaker.keel.persistence.KeelRepository
2038
import com.netflix.spinnaker.keel.slack.handlers.SlackNotificationHandler
2139
import com.netflix.spinnaker.keel.slack.handlers.supporting
2240
import com.netflix.spinnaker.keel.telemetry.ArtifactVersionVetoed
41+
import com.netflix.spinnaker.keel.telemetry.VerificationCompleted
2342
import org.slf4j.LoggerFactory
2443
import org.springframework.context.event.EventListener
2544
import org.springframework.stereotype.Component
@@ -62,7 +81,7 @@ class NotificationEventListener(
6281
application = config.application,
6382
time = clock.instant()
6483
),
65-
Type.ARTIFACT_PINNED,
84+
ARTIFACT_PINNED,
6685
pin.targetEnvironment)
6786
}
6887

@@ -95,7 +114,7 @@ class NotificationEventListener(
95114
user = user,
96115
targetEnvironment = targetEnvironment
97116
),
98-
Type.ARTIFACT_UNPINNED,
117+
ARTIFACT_UNPINNED,
99118
targetEnvironment)
100119

101120
log.debug("no environment $targetEnvironment was found in the config named ${config.name}")
@@ -125,7 +144,7 @@ class NotificationEventListener(
125144
application = config.name,
126145
comment = veto.comment
127146
),
128-
Type.ARTIFACT_MARK_AS_BAD,
147+
ARTIFACT_MARK_AS_BAD,
129148
veto.targetEnvironment
130149
)
131150
}
@@ -142,7 +161,7 @@ class NotificationEventListener(
142161
time = clock.instant(),
143162
application = application
144163
),
145-
Type.APPLICATION_PAUSED)
164+
APPLICATION_PAUSED)
146165
}
147166

148167
}
@@ -158,7 +177,7 @@ class NotificationEventListener(
158177
time = clock.instant(),
159178
application = application
160179
),
161-
Type.APPLICATION_RESUMED)
180+
APPLICATION_RESUMED)
162181
}
163182
}
164183

@@ -184,7 +203,7 @@ class NotificationEventListener(
184203
eventType = type,
185204
application = config.application
186205
),
187-
Type.LIFECYCLE_EVENT,
206+
LIFECYCLE_EVENT,
188207
artifact = deliveryArtifact)
189208
}
190209
}
@@ -210,7 +229,7 @@ class NotificationEventListener(
210229
priorVersion = priorVersion,
211230
status = DeploymentStatus.SUCCEEDED
212231
),
213-
Type.ARTIFACT_DEPLOYMENT,
232+
ARTIFACT_DEPLOYMENT_SUCCEDEED,
214233
targetEnvironment)
215234
}
216235
}
@@ -238,7 +257,7 @@ class NotificationEventListener(
238257
targetEnvironment = veto.targetEnvironment,
239258
status = DeploymentStatus.FAILED
240259
),
241-
Type.ARTIFACT_DEPLOYMENT,
260+
ARTIFACT_DEPLOYMENT_FAILED,
242261
veto.targetEnvironment)
243262
}
244263
}
@@ -257,7 +276,7 @@ class NotificationEventListener(
257276

258277
val deliveryArtifact = config.artifacts.find {
259278
it.reference == currentState.artifactReference
260-
} .also {
279+
}.also {
261280
if (it == null) log.debug("Artifact with reference ${currentState.artifactReference} not found in delivery config")
262281
} ?: return
263282

@@ -266,7 +285,7 @@ class NotificationEventListener(
266285
log.debug("$deliveryArtifact version ${currentState.artifactVersion} not found. Can't send manual judgement notification.")
267286
return
268287
}
269-
val currentArtifact = repository.getArtifactVersionByPromotionStatus(config, currentState.environmentName , deliveryArtifact, PromotionStatus.CURRENT)
288+
val currentArtifact = repository.getArtifactVersionByPromotionStatus(config, currentState.environmentName, deliveryArtifact, PromotionStatus.CURRENT)
270289

271290
sendSlackMessage(
272291
config,
@@ -279,12 +298,57 @@ class NotificationEventListener(
279298
deliveryArtifact = deliveryArtifact,
280299
stateUid = currentState.uid
281300
),
282-
Type.MANUAL_JUDGMENT,
301+
MANUAL_JUDGMENT_AWAIT,
283302
environment.name)
284303
}
285304
}
286305
}
287306

307+
@EventListener(VerificationCompleted::class)
308+
fun onVerificationCompletedNotification(notification: VerificationCompleted) {
309+
log.debug("Received verification completed event: $notification")
310+
with(notification) {
311+
if (status != ConstraintStatus.PASS && status != ConstraintStatus.FAIL) {
312+
log.debug("Not sending notification for verification completed with status $status it's not pass/fail. Ignoring notification for" +
313+
"application $application")
314+
return
315+
}
316+
val config = repository.getDeliveryConfig(notification.deliveryConfigName)
317+
318+
val deliveryArtifact = config.artifacts.find {
319+
it.reference == notification.artifactReference
320+
}.also {
321+
if (it == null) log.debug("Artifact with reference ${notification.artifactReference} not found in delivery config")
322+
} ?: return
323+
324+
val artifactVersion = repository.getArtifactVersion(deliveryArtifact, notification.artifactVersion, null)
325+
if (artifactVersion == null) {
326+
log.debug("artifact version is null for application ${config.application}. Can't send verification completed notification.")
327+
return
328+
}
329+
330+
val type = when (status) {
331+
ConstraintStatus.PASS -> TEST_PASSED
332+
ConstraintStatus.FAIL -> TEST_FAILED
333+
//We shouldn't get here as we checked prior that status is either fail/pass
334+
else -> TEST_PASSED
335+
}
336+
337+
sendSlackMessage(
338+
config,
339+
SlackVerificationCompletedNotification(
340+
time = clock.instant(),
341+
application = config.application,
342+
artifact = artifactVersion.copy(reference = deliveryArtifact.reference),
343+
targetEnvironment = environmentName,
344+
deliveryArtifact = deliveryArtifact,
345+
status = status
346+
),
347+
type,
348+
environmentName)
349+
}
350+
}
351+
288352

289353
private inline fun <reified T : SlackNotificationEvent> sendSlackMessage(config: DeliveryConfig, message: T, type: Type,
290354
targetEnvironment: String? = null,
@@ -306,10 +370,25 @@ class NotificationEventListener(
306370

307371
environments.flatMap { it.notifications }
308372
.filter { it.type == NotificationType.slack }
373+
.filter { translateFrequencyToEvents(it.frequency).contains(type) }
309374
.groupBy { it.address }
310375
.forEach { (channel, _) ->
311376
handler.sendMessage(message, channel)
312377
}
313378
}
379+
380+
381+
fun translateFrequencyToEvents(frequency: NotificationFrequency): List<Type> {
382+
val quietNotifications = listOf(ARTIFACT_MARK_AS_BAD, ARTIFACT_PINNED, ARTIFACT_UNPINNED, LIFECYCLE_EVENT, APPLICATION_PAUSED,
383+
APPLICATION_RESUMED, MANUAL_JUDGMENT_AWAIT, ARTIFACT_DEPLOYMENT_FAILED, TEST_FAILED)
384+
val normalNotifications = quietNotifications + listOf(ARTIFACT_DEPLOYMENT_SUCCEDEED, DELIVEY_CONFIG_UPDATED, TEST_PASSED)
385+
val verboseNotifications = normalNotifications + listOf(MANUAL_JUDGMENT_REJECTED, MANUAL_JUDGMENT_APPROVED)
386+
387+
return when (frequency) {
388+
verbose -> verboseNotifications
389+
normal -> normalNotifications
390+
quiet -> quietNotifications
391+
}
392+
}
314393
}
315394

keel-slack/src/main/kotlin/com/netflix/spinnaker/keel/slack/SlackNotificationEvent.kt

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.netflix.spinnaker.keel.slack
22

33
import com.netflix.spinnaker.keel.api.artifacts.DeliveryArtifact
44
import com.netflix.spinnaker.keel.api.artifacts.PublishedArtifact
5+
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus
56
import com.netflix.spinnaker.keel.core.api.EnvironmentArtifactPin
67
import com.netflix.spinnaker.keel.core.api.UID
78
import com.netflix.spinnaker.keel.lifecycle.LifecycleEventType
@@ -78,6 +79,15 @@ data class SlackManualJudgmentNotification(
7879
override val application: String
7980
) : SlackNotificationEvent(time, application)
8081

82+
data class SlackVerificationCompletedNotification(
83+
val artifact: PublishedArtifact,
84+
override val time: Instant,
85+
val targetEnvironment: String,
86+
val deliveryArtifact: DeliveryArtifact,
87+
val status: ConstraintStatus,
88+
override val application: String
89+
) : SlackNotificationEvent(time, application)
90+
8191

8292
enum class DeploymentStatus {
8393
SUCCEEDED, FAILED;

keel-slack/src/main/kotlin/com/netflix/spinnaker/keel/slack/SlackService.kt

+15-18
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ import com.netflix.spinnaker.config.SlackConfiguration
77
import com.netflix.spinnaker.keel.notifications.NotificationType
88
import com.slack.api.Slack
99
import com.slack.api.model.block.LayoutBlock
10-
import com.slack.api.webhook.Payload.PayloadBuilder
11-
import com.slack.api.webhook.WebhookPayloads.payload
1210
import org.slf4j.LoggerFactory
1311
import org.springframework.boot.context.properties.EnableConfigurationProperties
1412
import org.springframework.core.env.Environment
1513
import org.springframework.stereotype.Component
1614

1715
/**
18-
* This notifier is responsible for actually sending the Slack notification,
19-
* based on the [channel] and the [blocks] it gets from the different handlers.
16+
* This notifier is responsible for actually sending or fetching data from Slack directly.
2017
*/
2118
@Component
2219
@EnableConfigurationProperties(SlackConfiguration::class)
@@ -34,22 +31,28 @@ class SlackService(
3431
private val isSlackEnabled: Boolean
3532
get() = springEnv.getProperty("keel.notifications.slack", Boolean::class.java, true)
3633

34+
/**
35+
* Sends slack notification to [channel], which the specified [blocks].
36+
* In case of an error with creating the blocks, or for notification preview, the fallback text will be sent.
37+
*/
3738
fun sendSlackNotification(channel: String, blocks: List<LayoutBlock>,
38-
application: String, type: NotificationType) {
39+
application: String, type: List<NotificationType>,
40+
fallbackText: String) {
3941
if (isSlackEnabled) {
4042
log.debug("Sending slack notification $type for application $application in channel $channel")
4143

4244
val response = slack.methods(configToken).chatPostMessage { req ->
4345
req
4446
.channel(channel)
4547
.blocks(blocks)
48+
.text(fallbackText)
4649
}
4750

4851
if (response.isOk) {
4952
spectator.counter(
5053
SLACK_MESSAGE_SENT,
5154
listOf(
52-
BasicTag("notificationType", type.name),
55+
BasicTag("notificationType", type.first().name),
5356
BasicTag("application", application)
5457
)
5558
).safeIncrement()
@@ -67,6 +70,9 @@ class SlackService(
6770
}
6871
}
6972

73+
/**
74+
* Get slack username by the user's [email]. Return the original email if username is not found.
75+
*/
7076
fun getUsernameByEmail(email: String): String {
7177
log.debug("lookup user id for email $email")
7278
val response = slack.methods(configToken).usersLookupByEmail { req ->
@@ -84,6 +90,9 @@ class SlackService(
8490
return email
8591
}
8692

93+
/**
94+
* Get user's email address by slack [userId]. Return the original userId if email is not found.
95+
*/
8796
fun getEmailByUserId(userId: String): String {
8897
log.debug("lookup user email for username $userId")
8998
val response = slack.methods(configToken).usersInfo { req ->
@@ -103,18 +112,6 @@ class SlackService(
103112
return userId
104113
}
105114

106-
// Update a notification based on the response Url, using blocks (the actual notification).
107-
// If something failed, the fallback text will be displayed
108-
fun respondToCallback(responseUrl: String, blocks: List<LayoutBlock>, fallbackText: String) {
109-
val response = slack.send(responseUrl, payload { p: PayloadBuilder ->
110-
p
111-
.text(fallbackText)
112-
.blocks(blocks)
113-
})
114-
115-
log.debug("slack respondToCallback returned ${response.code}")
116-
}
117-
118115

119116
companion object {
120117
private const val SLACK_MESSAGE_SENT = "keel.slack.message.sent"

0 commit comments

Comments
 (0)