diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgent.groovy
index f2aee1825..13ccbc17c 100644
--- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgent.groovy
+++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgent.groovy
@@ -37,6 +37,10 @@ abstract class AbstractEventNotificationAgent implements EchoEventListener {
String spinnakerUrl
static Map CONFIG = [
+ 'orchestration': [
+ type: 'orchestration',
+ link: 'tasks'
+ ],
'pipeline': [
type: 'pipeline',
link: 'executions/details'
@@ -83,7 +87,7 @@ abstract class AbstractEventNotificationAgent implements EchoEventListener {
}
// TODO (lpollo): why do we have a 'CANCELED' status and a canceled property, which are prime for inconsistency?
- if (config.type == 'pipeline' &&
+ if (isExecution(config.type) &&
(event.content.execution?.status == 'CANCELED' || event.content.execution?.canceled == true)) {
return
}
@@ -95,10 +99,10 @@ abstract class AbstractEventNotificationAgent implements EchoEventListener {
def sendRequests = []
// pipeline level
- if (config.type == 'pipeline') {
- event.content?.execution.notifications?.each { notification ->
+ if (isExecution(config.type)) {
+ event.content?.execution?.notifications?.each { notification ->
String key = getNotificationType()
- if (notification.type == key && notification?.when?.contains("$config.type.$status".toString())) {
+ if (notification.type == key && notification?.when?.contains("${config.type}.$status".toString())) {
sendRequests << notification
}
}
@@ -110,7 +114,7 @@ abstract class AbstractEventNotificationAgent implements EchoEventListener {
if (event.content?.context?.sendNotifications && ( !isSynthetic ) ) {
event.content?.context?.notifications?.each { notification ->
String key = getNotificationType()
- if (notification.type == key && notification?.when?.contains("$config.type.$status".toString())) {
+ if (notification.type == key && notification?.when?.contains("${config.type}.$status".toString())) {
sendRequests << notification
}
}
@@ -118,10 +122,18 @@ abstract class AbstractEventNotificationAgent implements EchoEventListener {
}
sendRequests.each { notification ->
- sendNotifications(notification, application, event, config, status)
+ try {
+ sendNotifications(notification, application, event, config, status)
+ } catch (Exception e) {
+ log.error('failed to send {} message ', notificationType ,e)
+ }
}
}
+ private boolean isExecution(String type) {
+ return type == "pipeline" || type == "orchestration"
+ }
+
abstract String getNotificationType()
abstract void sendNotifications(Map notification, String application, Event event, Map config, String status)
diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy
index e1a5fbaa5..692532202 100644
--- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy
+++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy
@@ -49,52 +49,47 @@ class BearychatNotificationAgent extends AbstractEventNotificationAgent {
@Override
void sendNotifications(Map preference, String application, Event event, Map config, String status) {
- try {
- String buildInfo = ''
-
- if (config.type == 'pipeline' || config.type == 'stage') {
- if (event.content?.execution?.trigger?.buildInfo?.url) {
- buildInfo = """build #${
- event.content.execution.trigger.buildInfo.number as Integer
- } """
- }
- }
- log.info('Sending bearychat message {} for {} {} {} {}', kv('address', preference.address), kv('application', application), kv('type', config.type), kv('status', status), kv('executionId', event.content?.execution?.id))
-
+ String buildInfo = ''
- String message = ''
-
- if (config.type == 'stage') {
- String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
- message = """Stage $stageName for """
+ if (config.type == 'pipeline' || config.type == 'stage') {
+ if (event.content?.execution?.trigger?.buildInfo?.url) {
+ buildInfo = """build #${
+ event.content.execution.trigger.buildInfo.number as Integer
+ } """
}
+ }
+ log.info('Sending bearychat message {} for {} {} {} {}', kv('address', preference.address), kv('application', application), kv('type', config.type), kv('status', status), kv('executionId', event.content?.execution?.id))
- String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link }/${event.content?.execution?.id}"
- message +=
- """${WordUtils.capitalize(application)}'s ${
- event.content?.execution?.name ?: event.content?.execution?.description
- } ${buildInfo} ${config.type == 'task' ? 'task' : 'pipeline'} ${status == 'starting' ? 'is' : 'has'} ${
- status == 'complete' ? 'completed successfully' : status
- }. To see more details, please visit: ${link}"""
+ String message = ''
- String customMessage = event.content?.context?.customMessage
- if (customMessage) {
- message = customMessage
- .replace("{{executionId}}", (String) event.content.execution?.id ?: "")
- .replace("{{link}}", link ?: "")
- }
+ if (config.type == 'stage') {
+ String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
+ message = """Stage $stageName for """
+ }
- List userList = bearychatService.getUserList(token)
- String userid = userList.find {it.email == preference.address}.id
- CreateP2PChannelResponse channelInfo = bearychatService.createp2pchannel(token,new CreateP2PChannelPara(user_id: userid))
- String channelId = channelInfo.vchannel_id
- bearychatService.sendMessage(token,new SendMessagePara(vchannel_id: channelId,
- text: message,
- attachments: "" ))
+ String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link }/${event.content?.execution?.id}"
- } catch (Exception e) {
- log.error('failed to send bearychat message ', e)
+ message +=
+ """${WordUtils.capitalize(application)}'s ${
+ event.content?.execution?.name ?: event.content?.execution?.description
+ } ${buildInfo} ${config.type == 'task' ? 'task' : 'pipeline'} ${status == 'starting' ? 'is' : 'has'} ${
+ status == 'complete' ? 'completed successfully' : status
+ }. To see more details, please visit: ${link}"""
+
+ String customMessage = event.content?.context?.customMessage
+ if (customMessage) {
+ message = customMessage
+ .replace("{{executionId}}", (String) event.content.execution?.id ?: "")
+ .replace("{{link}}", link ?: "")
}
+
+ List userList = bearychatService.getUserList(token)
+ String userid = userList.find {it.email == preference.address}.id
+ CreateP2PChannelResponse channelInfo = bearychatService.createp2pchannel(token,new CreateP2PChannelPara(user_id: userid))
+ String channelId = channelInfo.vchannel_id
+ bearychatService.sendMessage(token,new SendMessagePara(vchannel_id: channelId,
+ text: message,
+ attachments: "" ))
}
}
diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GoogleChatNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GoogleChatNotificationAgent.groovy
index c30b2c2ed..6cc669025 100644
--- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GoogleChatNotificationAgent.groovy
+++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GoogleChatNotificationAgent.groovy
@@ -38,59 +38,54 @@ public class GoogleChatNotificationAgent extends AbstractEventNotificationAgent
@Override
public void sendNotifications(Map preference, String application, Event event, Map config, String status) {
- try {
- String buildInfo = ''
-
- if (config.type == 'pipeline' || config.type == 'stage') {
- if (event.content?.execution?.trigger?.buildInfo?.url) {
- buildInfo = """build #${
- event.content.execution.trigger.buildInfo.number as Integer
- } """
- }
- }
-
- log.info('Sending Google Chat message {} for {} {} {} {}', kv('address', preference.address), kv('application', application), kv('type', config.type), kv('status', status), kv('executionId', event.content?.execution?.id))
-
- String body = ''
+ String buildInfo = ''
- if (config.type == 'stage') {
- String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
- body = """Stage $stageName for """
+ if (config.type == 'pipeline' || config.type == 'stage') {
+ if (event.content?.execution?.trigger?.buildInfo?.url) {
+ buildInfo = """build #${
+ event.content.execution.trigger.buildInfo.number as Integer
+ } """
}
+ }
- String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link }/${event.content?.execution?.id}"
+ log.info('Sending Google Chat message {} for {} {} {} {}', kv('address', preference.address), kv('application', application), kv('type', config.type), kv('status', status), kv('executionId', event.content?.execution?.id))
- body +=
- """${capitalize(application)}'s ${
- event.content?.execution?.name ?: event.content?.execution?.description
- } ${buildInfo}${config.type == 'task' ? 'task' : 'pipeline'} ${status == 'starting' ? 'is' : 'has'} ${
- status == 'complete' ? 'completed successfully' : status
- }"""
+ String body = ''
- if (preference.message?."$config.type.$status"?.text) {
- body += "\n\n" + preference.message."$config.type.$status".text
- }
+ if (config.type == 'stage') {
+ String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
+ body = """Stage $stageName for """
+ }
- String customMessage = preference.customMessage ?: event.content?.context?.customMessage
- if (customMessage) {
- body = customMessage
- .replace("{{executionId}}", (String) event.content.execution?.id ?: "")
- .replace("{{link}}", link ?: "")
- }
+ String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link }/${event.content?.execution?.id}"
- // In Chat, users can only copy the whole link easily. We just extract the information from the whole link.
- // Example: https://chat.googleapis.com/v1/spaces/{partialWebhookUrl}
- String baseUrl = "https://chat.googleapis.com/v1/spaces/"
- String completeLink = preference.address
- String partialWebhookURL = completeLink.substring(baseUrl.length())
- Response response = googleChatService.sendMessage(partialWebhookURL, new GoogleChatMessage(body))
+ body +=
+ """${capitalize(application)}'s ${
+ event.content?.execution?.name ?: event.content?.execution?.description
+ } ${buildInfo}${config.type == 'task' ? 'task' : 'pipeline'} ${status == 'starting' ? 'is' : 'has'} ${
+ status == 'complete' ? 'completed successfully' : status
+ }"""
- log.info("Received response from Google Chat: {} {} for execution id {}. {}",
- response?.status, response?.reason, event.content?.execution?.id, response?.body)
+ if (preference.message?."$config.type.$status"?.text) {
+ body += "\n\n" + preference.message."$config.type.$status".text
+ }
- } catch (Exception e) {
- log.error('failed to send Google Chat message ', e)
+ String customMessage = preference.customMessage ?: event.content?.context?.customMessage
+ if (customMessage) {
+ body = customMessage
+ .replace("{{executionId}}", (String) event.content.execution?.id ?: "")
+ .replace("{{link}}", link ?: "")
}
+
+ // In Chat, users can only copy the whole link easily. We just extract the information from the whole link.
+ // Example: https://chat.googleapis.com/v1/spaces/{partialWebhookUrl}
+ String baseUrl = "https://chat.googleapis.com/v1/spaces/"
+ String completeLink = preference.address
+ String partialWebhookURL = completeLink.substring(baseUrl.length())
+ Response response = googleChatService.sendMessage(partialWebhookURL, new GoogleChatMessage(body))
+
+ log.info("Received response from Google Chat: {} {} for execution id {}. {}",
+ response?.status, response?.reason, event.content?.execution?.id, response?.body)
}
@Override
diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/SlackNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/SlackNotificationAgent.groovy
index d7d995bed..381417239 100644
--- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/SlackNotificationAgent.groovy
+++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/SlackNotificationAgent.groovy
@@ -46,75 +46,70 @@ class SlackNotificationAgent extends AbstractEventNotificationAgent {
@Override
void sendNotifications(Map preference, String application, Event event, Map config, String status) {
- try {
- String buildInfo = ' '
+ String buildInfo = ' '
- String color = '#CCCCCC'
+ String color = '#CCCCCC'
- if (status == 'failed') {
- color = '#B82525'
- }
+ if (status == 'failed') {
+ color = '#B82525'
+ }
- if (status == 'starting') {
- color = '#2275B8'
- }
+ if (status == 'starting') {
+ color = '#2275B8'
+ }
- if (status == 'complete') {
- color = '#769D3E'
- }
+ if (status == 'complete') {
+ color = '#769D3E'
+ }
- if (config.type == 'pipeline' || config.type == 'stage') {
- if (event.content?.execution?.trigger?.buildInfo?.url) {
- buildInfo = """ build <${event.content.execution.trigger.buildInfo.url}|${
- event.content.execution.trigger.buildInfo.number as Integer
- }> """
- }
+ if (config.type == 'pipeline' || config.type == 'stage') {
+ if (event.content?.execution?.trigger?.buildInfo?.url) {
+ buildInfo = """ build <${event.content.execution.trigger.buildInfo.url}|${
+ event.content.execution.trigger.buildInfo.number as Integer
+ }> """
}
+ }
- log.info('Sending Slack message {} for {} {} {} {}', kv('address', preference.address), kv('application', application), kv('type', config.type), kv('status', status), kv('executionId', event.content?.execution?.id))
+ log.info('Sending Slack message {} for {} {} {} {}', kv('address', preference.address), kv('application', application), kv('type', config.type), kv('status', status), kv('executionId', event.content?.execution?.id))
- String body = ''
+ String body = ''
- if (config.type == 'stage') {
- String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
- body = """Stage $stageName for """
- }
-
- String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link }/${event.content?.execution?.id}"
+ if (config.type == 'stage') {
+ String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
+ body = """Stage $stageName for """
+ }
- body +=
- """${capitalize(application)}'s <${link}|${
- event.content?.execution?.name ?: event.content?.execution?.description
- }>${buildInfo}${config.type == 'task' ? 'task' : 'pipeline'} ${status == 'starting' ? 'is' : 'has'} ${
- status == 'complete' ? 'completed successfully' : status
- }"""
+ String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link }/${event.content?.execution?.id}"
- if (preference.message?."$config.type.$status"?.text) {
- body += "\n\n" + preference.message."$config.type.$status".text
- }
+ body +=
+ """${capitalize(application)}'s <${link}|${
+ event.content?.execution?.name ?: event.content?.execution?.description
+ }>${buildInfo}${config.type == 'task' ? 'task' : 'pipeline'} ${status == 'starting' ? 'is' : 'has'} ${
+ status == 'complete' ? 'completed successfully' : status
+ }"""
- String customMessage = preference.customMessage ?: event.content?.context?.customMessage
- if (customMessage) {
- body = customMessage
- .replace("{{executionId}}", (String) event.content.execution?.id ?: "")
- .replace("{{link}}", link ?: "")
- }
+ if (preference.message?."$config.type.$status"?.text) {
+ body += "\n\n" + preference.message."$config.type.$status".text
+ }
- String address = preference.address.startsWith('#') ? preference.address : "#${preference.address}"
+ String customMessage = preference.customMessage ?: event.content?.context?.customMessage
+ if (customMessage) {
+ body = customMessage
+ .replace("{{executionId}}", (String) event.content.execution?.id ?: "")
+ .replace("{{link}}", link ?: "")
+ }
- Response response
- if (sendCompactMessages) {
- response = slackService.sendCompactMessage(token, new CompactSlackMessage(body, color), address, true)
- } else {
- String title = getNotificationTitle(config.type, application, status)
- response = slackService.sendMessage(token, new SlackAttachment(title, body, color), address, true)
- }
- log.info("Received response from Slack: {} {} for execution id {}. {}",
- response?.status, response?.reason, event.content?.execution?.id, response?.body)
+ String address = preference.address.startsWith('#') ? preference.address : "#${preference.address}"
- } catch (Exception e) {
- log.error('failed to send slack message ', e)
+ Response response
+ if (sendCompactMessages) {
+ response = slackService.sendCompactMessage(token, new CompactSlackMessage(body, color), address, true)
+ } else {
+ String title = getNotificationTitle(config.type, application, status)
+ response = slackService.sendMessage(token, new SlackAttachment(title, body, color), address, true)
}
+ log.info("Received response from Slack: {} {} for execution id {}. {}",
+ response?.status, response?.reason, event.content?.execution?.id, response?.body)
}
/**
diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy
index 639c278f6..2a45d322f 100644
--- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy
+++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy
@@ -41,46 +41,41 @@ class TwilioNotificationAgent extends AbstractEventNotificationAgent {
@Override
void sendNotifications(Map preference, String application, Event event, Map config, String status) {
- try {
- String name = event.content?.execution?.name ?: event.content?.execution?.description
- String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link}/${event.content?.execution?.id}"
+ String name = event.content?.execution?.name ?: event.content?.execution?.description
+ String link = "${spinnakerUrl}/#/applications/${application}/${config.type == 'stage' ? 'executions/details' : config.link}/${event.content?.execution?.id}"
- String buildInfo = ''
+ String buildInfo = ''
- if (config.type == 'pipeline') {
- if (event.content?.execution?.trigger?.buildInfo?.url) {
- buildInfo = """build #${event.content.execution.trigger.buildInfo.number as Integer} """
- }
+ if (config.type == 'pipeline') {
+ if (event.content?.execution?.trigger?.buildInfo?.url) {
+ buildInfo = """build #${event.content.execution.trigger.buildInfo.number as Integer} """
}
+ }
- log.info("Twilio: sms for ${preference.address} - ${link}")
-
- String message = ''
+ log.info("Twilio: sms for ${preference.address} - ${link}")
- if (config.type == 'stage') {
- String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
- message = """Stage $stageName for """
- }
+ String message = ''
- message +=
- """${WordUtils.capitalize(application)}'s ${
- event.content?.execution?.name ?: event.content?.execution?.description
- } ${buildInfo} ${config.type == 'task' ? 'task' : 'pipeline'} ${buildInfo} ${
- status == 'starting' ? 'is' : 'has'
- } ${
- status == 'complete' ? 'completed successfully' : status
- } ${link}"""
-
- twilioService.sendMessage(
- account,
- from,
- preference.address,
- message
- )
-
- } catch (Exception e) {
- log.error('failed to send sms message ', e)
+ if (config.type == 'stage') {
+ String stageName = event.content.name ?: event.content?.context?.stageDetails?.name
+ message = """Stage $stageName for """
}
+
+ message +=
+ """${WordUtils.capitalize(application)}'s ${
+ event.content?.execution?.name ?: event.content?.execution?.description
+ } ${buildInfo} ${config.type == 'task' ? 'task' : 'pipeline'} ${buildInfo} ${
+ status == 'starting' ? 'is' : 'has'
+ } ${
+ status == 'complete' ? 'completed successfully' : status
+ } ${link}"""
+
+ twilioService.sendMessage(
+ account,
+ from,
+ preference.address,
+ message
+ )
}
@Override
diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgentSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgentSpec.groovy
index bdd0e09e0..00dd3de18 100644
--- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgentSpec.groovy
+++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/AbstractEventNotificationAgentSpec.groovy
@@ -71,6 +71,9 @@ class AbstractEventNotificationAgentSpec extends Specification {
// notifications ON, another check for cancelled pipeline (should skip notifications)
fakePipelineEvent("orca:pipeline:failed", "WHATEVER", "pipeline.failed", [canceled: true]) || 0
+ fakeOrchestrationEvent("orca:orchestration:complete", "SUCCEEDED", "orchestration.complete")|| 1
+ fakeOrchestrationEvent("orca:orchestration:failed", "TERMINAL", "orchestration.failed") || 1
+
// notifications OFF, stage complete
fakeStageEvent("orca:stage:complete", null) || 0
// notifications OFF, stage failed
@@ -110,6 +113,27 @@ class AbstractEventNotificationAgentSpec extends Specification {
return new Event(eventProps)
}
+ private def fakeOrchestrationEvent(String type, String status, String notifyWhen, Map extraExecutionProps = [:]) {
+ def eventProps = [
+ details: [type: type],
+ content: [
+ execution: [
+ id: "1",
+ name: "foo-orchestration",
+ status: status
+ ]
+ ]
+ ]
+
+ if (notifyWhen) {
+ eventProps.content.execution << [notifications: [[type: "fake", when: "${notifyWhen}"]]]
+ }
+
+ eventProps.content.execution << extraExecutionProps
+
+ return new Event(eventProps)
+ }
+
private def fakeStageEvent(String type, String notifyWhen, canceled = false, synthetic = false) {
def eventProps = [
details: [type: type],