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],