Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Initial commit
  • Loading branch information
tmclaugh committed May 10, 2018
0 parents commit d64b450
Show file tree
Hide file tree
Showing 18 changed files with 340 additions and 0 deletions.
41 changes: 41 additions & 0 deletions .gitignore
@@ -0,0 +1,41 @@
# Compiled stuff
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# Dev
.mypy_cache/
.pytest_cache/

# pyenv
.python-version

# Serverless directories
.serverless

# Serverless plugins / NPM.
package-lock.json
node_modules/

# IDE
.settings/
.project
.pydevproject
26 changes: 26 additions & 0 deletions LICENSE
@@ -0,0 +1,26 @@
BSD 2-Clause License

Copyright (c) 2018, ServerlessOps, LLC
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

11 changes: 11 additions & 0 deletions README.md
@@ -0,0 +1,11 @@
# aws-sns-to-slack-publisher
[![Serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com)
[![License](https://img.shields.io/badge/License-BSD%202--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause)

Publish a message received from SNS to Slack

![System Architecture](/diagram.png?raw=true "System Architecture")

## Outputs

* __aws-sns-to-slack-publisherr-${stage}-SlackResponseSnsTopicArn__: Topic to which Slack publishing responses are sent.
Binary file added diagram.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions handlers/aws_sns_to_slack_publisher.py
@@ -0,0 +1,64 @@
'''Publish message from SNS to Slack'''

import json
import logging
import os

import boto3
from slackclient import SlackClient

log_level = os.environ.get('LOG_LEVEL', 'INFO')
logging.root.setLevel(logging.getLevelName(log_level)) # type: ignore
_logger = logging.getLogger(__name__)

SLACK_API_TOKEN = os.environ.get('SLACK_API_TOKEN')
SLACK_DEFAULT_CHANNEL = os.environ.get('SLACK_DEFAULT_CHANNEL')
SLACK = SlackClient(SLACK_API_TOKEN)

RESPONSE_SNS_TOPIC_ARN = os.environ.get('RESPONSE_SNS_TOPIC_ARN')
SNS = boto3.client('sns')


def _get_message_from_event(event: dict) -> dict:
'''Get the message from the event'''
return json.loads(event.get('Records')[0].get('Sns').get('Message'))


def _publish_slack_message(channel: str, message: dict) -> dict:
'''Publish message to Slack'''
r = SLACK.api_call(
"chat.postMessage",
channel=json.dumps(channel),
text=message
)
_logger.debug('Slack response: {}'.format(json.dumps(r)))
return r


def _publish_sns_message(sns_topic_arn: str, message: str) -> dict:
'''Publish message to SNS topic'''
r = SNS.publish(
TopicArn=sns_topic_arn,
Message=message
)
_logger.debug('SNS response: {}'.format(json.dumps(r)))

return r


def handler(event, context):
'''Function entry'''
_logger.debug('Event received: {}'.format(json.dumps(event)))
slack_message = _get_message_from_event(event)
slack_response = _publish_slack_message(SLACK_DEFAULT_CHANNEL, slack_message)
sns_response = _publish_sns_message(RESPONSE_SNS_TOPIC_ARN, json.dumps(slack_response))

resp = {
'slack_response': slack_response,
'sns_response': sns_response,
'status': 'OK'
}

_logger.debug('Response: {}'.format(json.dumps(resp)))
return resp

10 changes: 10 additions & 0 deletions package.json
@@ -0,0 +1,10 @@
{
"name": "aws-sns-to-slack-publisher",
"description": "",
"version": "0.1.0",
"dependencies": {},
"devDependencies": {
"serverless-iam-roles-per-function": "^0.1.5",
"serverless-python-requirements": "^4.0.3"
}
}
2 changes: 2 additions & 0 deletions pytest.ini
@@ -0,0 +1,2 @@
[pytest]
testpaths = tests handlers
7 changes: 7 additions & 0 deletions requirements-dev.txt
@@ -0,0 +1,7 @@
flake8
moto
mypy
pylint
pytest
pytest-pylint

5 changes: 5 additions & 0 deletions requirements.txt
@@ -0,0 +1,5 @@
# serverless-python-requirements will strip boto3, but we have it here to make local
# testing tools happy.
boto3
slackclient

64 changes: 64 additions & 0 deletions serverless.yml
@@ -0,0 +1,64 @@
# <DESCRIBE SERVICE>
service: aws-sns-to-slack-publisher

plugins:
- serverless-python-requirements
- serverless-iam-roles-per-function


custom:
stage: "${opt:stage, env:SLS_STAGE, 'dev'}"
profile: "${opt:aws-profile, env:AWS_PROFILE, env:AWS_DEFAULT_PROFILE, 'default'}"
log_level: "${env:LOG_LEVEL, 'INFO'}"

slack_api_token: "${env:SLACK_API_TOKEN, 'INSERT_TOKEN'}"
slack_default_channel: "${env:SLACK_DEFAULT_CHANNEL, '#notifications'}"

pythonRequirements:
dockerizePip: false


provider:
name: aws
profile: ${self:custom.profile}
stage: ${self:custom.stage}
runtime: python3.6
environment:
LOG_LEVEL: ${self:custom.log_level}
stackTags:
x-service: aws-sns-to-slack-publisher
x-stack: ${self:service}-${self:provider.stage}


functions:
SlackPublish:
handler: handlers/aws_sns_to_slack_publisher.handler
description: "Publish message from SNS to Slack"
memorySize: 128
timeout: 10
iamRoleStatements:
- Effect: "Allow"
Action: "SNS:Publish"
Resource:
Ref: SlackResponseSnsTopic
environment:
SLACK_API_TOKEN: "${self:custom.slack_api_token}"
SLACK_DEFAULT_CHANNEL: "${self:custom.slack_default_channel}"
RESPONSE_SNS_TOPIC_ARN:
Ref: SlackResponseSnsTopic


resources:
Resources:
SlackResponseSnsTopic:
Type: "AWS::SNS::Topic"


Outputs:
SlackResponseSnsTopicArn:
Description: "AWS SNS Topic ARN"
Value:
Ref: SlackResponseSnsTopic
Export:
Name: "${self:service}-${self:provider.stage}-SlackResponseSnsTopicArn"

Empty file added tests/__init__.py
Empty file.
1 change: 1 addition & 0 deletions tests/conftest.py
@@ -0,0 +1 @@
'''Pytest config'''
31 changes: 31 additions & 0 deletions tests/events/aws_sns_to_slack_publisher.json
@@ -0,0 +1,31 @@
{
"Records": [
{
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:us-east-1:000000000000:test-topic:0c31b8f8-398c-4bb0-ad4a-437b7113ad06",
"EventSource": "aws:sns",
"Sns": {
"SignatureVersion": "1",
"Timestamp": "1970-01-01T00:00:00.000Z",
"Signature": "EXAMPLE",
"SigningCertUrl": "EXAMPLE",
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
"Message": "{\"text\": \"AWS-SNS-TO-SLACK-PUBLISHER TEST MESSAGE\"}",
"MessageAttributes": {
"Test": {
"Type": "String",
"Value": "TestString"
},
"TestBinary": {
"Type": "Binary",
"Value": "TestBinary"
}
},
"Type": "Notification",
"UnsubscribeUrl": "EXAMPLE",
"TopicArn": "arn:aws:sns:us-east-1:000000000000:test-topic",
"Subject": "TestInvoke"
}
}
]
}
Empty file added tests/integration/__init__.py
Empty file.
Empty file.
Empty file added tests/unit/__init__.py
Empty file.
Empty file added tests/unit/handlers/__init__.py
Empty file.
78 changes: 78 additions & 0 deletions tests/unit/handlers/test_aws_sns_to_slack_publisher.py
@@ -0,0 +1,78 @@
'''Test health_event_publisher'''
# pylint: disable=protected-access
# pylint: disable=wrong-import-position
# pylint: disable=redefined-outer-name
import json
import os

import boto3
from moto import mock_sns, mock_sts
import pytest

import handlers.aws_sns_to_slack_publisher as h

EVENT_FILE = os.path.join(
os.path.dirname(__file__),
'..',
'..',
'events',
'aws_sns_to_slack_publisher.json'
)

SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:000000000000:test-topic'
SLACK_CHANNEL = "#testing"


@pytest.fixture()
def event(event_file=EVENT_FILE):
'''Trigger event'''
with open(event_file) as f:
return json.load(f)

@pytest.fixture()
def slack_message(event):
'''Slack message'''
return json.dumps(h._get_message_from_event(event))


@pytest.fixture()
def slack_channel(channel=SLACK_CHANNEL):
'''Slack channel'''
return channel


@pytest.fixture()
def slack_response():
'''Slack response'''
return {}


@pytest.fixture()
def sns_message(slack_response):
'''SNS message'''
return slack_response


@pytest.fixture()
def sns_topic_arn(topic_arn=SNS_TOPIC_ARN):
'''SNS Topic ARN'''
return topic_arn


def test__get_message_from_event(event):
'''Test fetching message from test event.'''
r = h._get_message_from_event(event)

assert False


def test__publish_slack_message(slack_channel, slack_message):
'''Test publish a test slack message.'''
assert False


@mock_sts
@mock_sns
def test__publish_sns_message(sns_topic_arn, sns_message):
'''Test publish an SNS message.'''
assert False

0 comments on commit d64b450

Please sign in to comment.