From 03b6d58ab4e0778f99412f532e343bffc87e30c8 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Jan 2019 12:57:46 -0800 Subject: [PATCH] Support subscribing to SNS ARNs This updates `@app.on_sns_message` to accept either the topic name or the topic ARN rather than just the name. While accepting the name can be a simpler experience, it prevents users from subscribing to topics that are in other regions or other accounts. --- CHANGELOG.rst | 2 ++ chalice/deploy/planner.py | 56 +++++++++++++++++------------- docs/source/api.rst | 3 +- docs/source/topics/events.rst | 4 +++ tests/unit/deploy/test_planner.py | 57 +++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cce901c59..f6c3017ee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ Next Release (TBD) * Fix packaging multiple local directories as dependencies (`#1047 `__) +* Add support for passing SNS ARNs to ``on_sns_message`` + (`#1048 `__) 1.6.2 diff --git a/chalice/deploy/planner.py b/chalice/deploy/planner.py index b49de115f..282e006e9 100644 --- a/chalice/deploy/planner.py +++ b/chalice/deploy/planner.py @@ -439,31 +439,41 @@ def _plan_snslambdasubscription(self, resource): ) topic_arn_varname = '%s_topic_arn' % resource.resource_name subscribe_varname = '%s_subscription_arn' % resource.resource_name - # To keep the user API simple, we only require the topic - # name and not the ARN. However, the APIs require the topic - # ARN so we need to reconstruct it here in the planner. - instruction_for_topic_arn = [ - models.BuiltinFunction( - 'parse_arn', - [function_arn], - output_var='parsed_lambda_arn', - ), - models.JPSearch('account_id', - input_var='parsed_lambda_arn', - output_var='account_id'), - models.JPSearch('region', - input_var='parsed_lambda_arn', - output_var='region_name'), - models.StoreValue( - name=topic_arn_varname, - value=StringFormat( - 'arn:aws:sns:{region_name}:{account_id}:%s' % ( - resource.topic + + instruction_for_topic_arn = [] # type: List[InstructionMsg] + if resource.topic.startswith('arn:aws:sns:'): + instruction_for_topic_arn += [ + models.StoreValue( + name=topic_arn_varname, + value=resource.topic, + ) + ] + else: + # To keep the user API simple, we only require the topic + # name and not the ARN. However, the APIs require the topic + # ARN so we need to reconstruct it here in the planner. + instruction_for_topic_arn += [ + models.BuiltinFunction( + 'parse_arn', + [function_arn], + output_var='parsed_lambda_arn', + ), + models.JPSearch('account_id', + input_var='parsed_lambda_arn', + output_var='account_id'), + models.JPSearch('region', + input_var='parsed_lambda_arn', + output_var='region_name'), + models.StoreValue( + name=topic_arn_varname, + value=StringFormat( + 'arn:aws:sns:{region_name}:{account_id}:%s' % ( + resource.topic + ), + ['region_name', 'account_id'], ), - ['region_name', 'account_id'], ), - ), - ] # type: List[InstructionMsg] + ] if self._remote_state.resource_exists(resource): # Given there's nothing about an SNS subscription you can # configure for now, if the resource exists, we don't do diff --git a/docs/source/api.rst b/docs/source/api.rst index 110180463..5b80df862 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -235,8 +235,7 @@ Chalice app.log.info("SNS subject: %s", event.subject) app.log.info("SNS message: %s", event.message) - :param topic: The name of the SNS topic you want to subscribe to. - This is the name of the topic, not the topic ARN. + :param topic: The name or ARN of the SNS topic you want to subscribe to. :param name: The name of the function to use. This name is combined with the chalice app name as well as the stage name to create the diff --git a/docs/source/topics/events.rst b/docs/source/topics/events.rst index 9d96b7fbb..d8b744de1 100644 --- a/docs/source/topics/events.rst +++ b/docs/source/topics/events.rst @@ -208,6 +208,10 @@ command:: 2018-06-28 17:49:30.513000 547e0f chalice-demo-sns - DEBUG - Received message with subject: TestSubject1, message: TestMessage1 2018-06-28 17:49:40.391000 547e0f chalice-demo-sns - DEBUG - Received message with subject: TestSubject2, message: TestMessage2 +In this example we used the SNS topic name to register our handler, but you can +also use the topic arn. This can be useful if your topic is in another region +or account. + .. _sqs-events: diff --git a/tests/unit/deploy/test_planner.py b/tests/unit/deploy/test_planner.py index 9fbd125a5..7a46302a5 100644 --- a/tests/unit/deploy/test_planner.py +++ b/tests/unit/deploy/test_planner.py @@ -660,6 +660,63 @@ def test_can_plan_sns_subscription(self): ), ] + def test_can_plan_sns_arn_subscription(self): + function = create_function_resource('function_name') + topic_arn = 'arn:aws:sns:mars-west-2:123456789:mytopic' + sns_subscription = models.SNSLambdaSubscription( + resource_name='function_name-sns-subscription', + topic=topic_arn, + lambda_function=function + ) + plan = self.determine_plan(sns_subscription) + plan_parse_arn = plan[0] + assert plan_parse_arn == models.StoreValue( + name='function_name-sns-subscription_topic_arn', + value=topic_arn, + ) + topic_arn_var = Variable("function_name-sns-subscription_topic_arn") + assert plan[1:] == [ + models.APICall( + method_name='add_permission_for_sns_topic', + params={ + 'function_arn': Variable("function_name_lambda_arn"), + 'topic_arn': topic_arn_var, + }, + output_var=None + ), + models.APICall( + method_name='subscribe_function_to_topic', + params={ + 'function_arn': Variable("function_name_lambda_arn"), + 'topic_arn': topic_arn_var, + }, + output_var='function_name-sns-subscription_subscription_arn' + ), + models.RecordResourceValue( + resource_type='sns_event', + resource_name='function_name-sns-subscription', + name='topic', + value=topic_arn), + models.RecordResourceVariable( + resource_type='sns_event', + resource_name='function_name-sns-subscription', + name='lambda_arn', + variable_name='function_name_lambda_arn' + ), + models.RecordResourceVariable( + resource_type='sns_event', + resource_name='function_name-sns-subscription', + name='subscription_arn', + variable_name='function_name-sns-subscription_subscription_arn' + ), + models.RecordResourceVariable( + resource_type='sns_event', + resource_name='function_name-sns-subscription', + name='topic_arn', + variable_name='function_name-sns-subscription_topic_arn', + ), + ] + def test_sns_subscription_exists_is_noop_for_planner(self): function = create_function_resource('function_name') sns_subscription = models.SNSLambdaSubscription(