Repository: cloud-custodian/cloud-custodian

Issue:
SNS transport
Notifications capability should have an SNS transport option for real-time notifications.



Discussion:
two things, i've got support in a branch for sns transport, but its not going to be very useful as its going to be a raw json message. second, the latency around the sqs should be under a minute, we're basically on a 5m timer with lambda, and the lambda delivery loops on the queue for 4 minutes.

Under a minute is probably sufficient for most use cases - good to know it's that fast now.

In addition, SNS has other applications other than real time. It would be nice to be able to list an SNS topic and have people subscribe as they see fit so we can get new subscribers to notifications without updating custodian config files.

The json issue is a little more difficult. Have to think about this one some more...

This is dependent on #163 


Resolving patch:
diff --git a/c7n/actions.py b/c7n/actions.py
--- a/c7n/actions.py
+++ b/c7n/actions.py
@@ -345,15 +345,22 @@ class Notify(EventAction):
             'subject': {'type': 'string'},
             'template': {'type': 'string'},
             'transport': {
-                'type': 'object',
-                'required': ['type', 'queue'],
-                'properties': {
-                    'queue': {'type': 'string'},
-                    'region': {'type': 'string'},
-                    'type': {'enum': ['sqs']}}
+                'oneOf': [
+                    {'type': 'object',
+                     'required': ['type', 'queue'],
+                     'properties': {
+                         'queue': {'type': 'string'},
+                         'type': {'enum': ['sqs']}}},
+                    {'type': 'object',
+                     'required': ['type', 'topic'],
+                     'properties': {
+                         'topic': {'type': 'string'},
+                         'type': {'enum': ['sns']},
+                         }}]
             }
         }
     }
+
     batch_size = 250
 
     def process(self, resources, event=None):
@@ -375,6 +382,17 @@ def process(self, resources, event=None):
     def send_data_message(self, message):
         if self.data['transport']['type'] == 'sqs':
             return self.send_sqs(message)
+        elif self.data['transport']['type'] == 'sns':
+            return self.send_sns(message)
+
+    def send_sns(self, message):
+        topic = self.data['transport']['topic']
+        region = topic.split(':', 5)[3]
+        client = self.manager.session_factory(region=region).client('sns')
+        client.publish(
+            TopicArn=topic,
+            Message=base64.b64encode(zlib.compress(utils.dumps(message)))
+            )
 
     def send_sqs(self, message):
         queue = self.data['transport']['queue']


Test patch:
diff --git a/tests/data/placebo/test_sns_notify_action/iam.ListAccountAliases_1.json b/tests/data/placebo/test_sns_notify_action/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_sns_notify_action/iam.ListAccountAliases_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200, 
+    "data": {
+        "AccountAliases": [
+            "custodian-skunk-works"
+        ], 
+        "IsTruncated": false, 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "cf1d538f-d426-11e6-aa54-495189d818bc", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "cf1d538f-d426-11e6-aa54-495189d818bc", 
+                "date": "Fri, 06 Jan 2017 15:43:02 GMT", 
+                "content-length": "400", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sns_notify_action/sns.CreateTopic_1.json b/tests/data/placebo/test_sns_notify_action/sns.CreateTopic_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_sns_notify_action/sns.CreateTopic_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "4f0e6c0f-cdad-55bf-afa0-3ce670909709", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "4f0e6c0f-cdad-55bf-afa0-3ce670909709", 
+                "date": "Fri, 06 Jan 2017 15:42:53 GMT", 
+                "content-length": "322", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "TopicArn": "arn:aws:sns:us-east-2:644160558196:c7n-notify-test"
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sns_notify_action/sns.DeleteTopic_1.json b/tests/data/placebo/test_sns_notify_action/sns.DeleteTopic_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_sns_notify_action/sns.DeleteTopic_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "9ce63189-3a15-536d-95eb-61608758cf79", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "9ce63189-3a15-536d-95eb-61608758cf79", 
+                "date": "Fri, 06 Jan 2017 15:43:08 GMT", 
+                "content-length": "201", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sns_notify_action/sns.GetTopicAttributes_1.json b/tests/data/placebo/test_sns_notify_action/sns.GetTopicAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_sns_notify_action/sns.GetTopicAttributes_1.json
@@ -0,0 +1,27 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Attributes": {
+            "SubscriptionsConfirmed": "0", 
+            "DisplayName": "", 
+            "SubscriptionsDeleted": "0", 
+            "EffectiveDeliveryPolicy": "{\"http\":{\"defaultHealthyRetryPolicy\":{\"minDelayTarget\":20,\"maxDelayTarget\":20,\"numRetries\":3,\"numMaxDelayRetries\":0,\"numNoDelayRetries\":0,\"numMinDelayRetries\":0,\"backoffFunction\":\"linear\"},\"disableSubscriptionOverrides\":false}}", 
+            "Owner": "644160558196", 
+            "Policy": "{\"Version\":\"2008-10-17\",\"Id\":\"__default_policy_ID\",\"Statement\":[{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:GetTopicAttributes\",\"SNS:SetTopicAttributes\",\"SNS:AddPermission\",\"SNS:RemovePermission\",\"SNS:DeleteTopic\",\"SNS:Subscribe\",\"SNS:ListSubscriptionsByTopic\",\"SNS:Publish\",\"SNS:Receive\"],\"Resource\":\"arn:aws:sns:us-east-2:644160558196:c7n-notify-test\",\"Condition\":{\"StringEquals\":{\"AWS:SourceOwner\":\"644160558196\"}}}]}", 
+            "TopicArn": "arn:aws:sns:us-east-2:644160558196:c7n-notify-test", 
+            "SubscriptionsPending": "0"
+        }, 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "3d0042d1-8231-59c9-866e-12383f724d1c", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "3d0042d1-8231-59c9-866e-12383f724d1c", 
+                "vary": "Accept-Encoding", 
+                "content-length": "2147", 
+                "content-type": "text/xml", 
+                "date": "Fri, 06 Jan 2017 15:42:58 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sns_notify_action/sns.ListTopics_1.json b/tests/data/placebo/test_sns_notify_action/sns.ListTopics_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_sns_notify_action/sns.ListTopics_1.json
@@ -0,0 +1,21 @@
+{
+    "status_code": 200, 
+    "data": {
+        "Topics": [
+            {
+                "TopicArn": "arn:aws:sns:us-east-2:644160558196:c7n-notify-test"
+            }
+        ], 
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "def28cb1-b2cf-564c-84c8-dc580f8f2d7e", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "def28cb1-b2cf-564c-84c8-dc580f8f2d7e", 
+                "date": "Fri, 06 Jan 2017 15:42:56 GMT", 
+                "content-length": "380", 
+                "content-type": "text/xml"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sns_notify_action/sns.Publish_1.json b/tests/data/placebo/test_sns_notify_action/sns.Publish_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_sns_notify_action/sns.Publish_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "RetryAttempts": 0, 
+            "HTTPStatusCode": 200, 
+            "RequestId": "d3da9fe9-5057-5b81-b495-cf00ed1e0292", 
+            "HTTPHeaders": {
+                "x-amzn-requestid": "d3da9fe9-5057-5b81-b495-cf00ed1e0292", 
+                "date": "Fri, 06 Jan 2017 15:43:05 GMT", 
+                "content-length": "294", 
+                "content-type": "text/xml"
+            }
+        }, 
+        "MessageId": "877c1ca5-908d-5aae-abfe-4c4c2dc423bd"
+    }
+}
\ No newline at end of file
diff --git a/tests/test_notify.py b/tests/test_notify.py
--- a/tests/test_notify.py
+++ b/tests/test_notify.py
@@ -12,12 +12,33 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
 from common import BaseTest
 
 
 class NotifyTest(BaseTest):
 
+    def test_sns_notify(self):
+        session_factory = self.replay_flight_data(
+            'test_sns_notify_action')
+        client = session_factory().client('sns')
+        topic = client.create_topic(Name='c7n-notify-test')['TopicArn']
+        self.addCleanup(client.delete_topic, TopicArn=topic)
+
+        policy = self.load_policy({
+            'name': 'notify-sns',
+            'resource': 'sns',
+            'filters': [
+                {'TopicArn': topic}],
+            'actions': [{
+                'type': 'notify',
+                'to': ['noone@example.com'],
+                'transport': {
+                    'type': 'sns',
+                    'topic': topic}}]
+            }, session_factory=session_factory)
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        
     def test_notify(self):
         session_factory = self.replay_flight_data(
             "test_notify_action", zdata=True)