Skip to content

Commit 7b4c7b0

Browse files
committed
feat: add AWS::Serverless::LayerVersion and AWS::Serverless::Application (#688)
1 parent 75cb2e8 commit 7b4c7b0

File tree

81 files changed

+3984
-98
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+3984
-98
lines changed

docs/cloudformation_compatibility.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Tracing All
6060
KmsKeyArn All
6161
DeadLetterQueue All
6262
DeploymentPreference All
63+
Layers All
6364
AutoPublishAlias Ref of a CloudFormation Parameter Alias resources created by SAM uses a LocicalId <FunctionLogicalId+AliasName>. So SAM either needs a string for alias name, or a Ref to template Parameter that SAM can resolve into a string.
6465
ReservedConcurrentExecutions All
6566
============================ ================================== ========================
@@ -171,6 +172,20 @@ Cors All
171172
================================== ======================== ========================
172173

173174

175+
AWS::Serverless::Application
176+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
177+
178+
================================== ======================== ========================
179+
Property Name Intrinsic(s) Supported Reasons
180+
================================== ======================== ========================
181+
Location None SAM expects exact values for the Location property
182+
Parameters All
183+
NotificationArns All
184+
Tags All
185+
TimeoutInMinutes All
186+
================================== ======================== ========================
187+
188+
174189
AWS::Serverless::SimpleTable
175190
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
176191

docs/globals.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Currently, the following resources and properties are being supported:
6464
Tags:
6565
Tracing:
6666
KmsKeyArn:
67+
Layers:
6768
AutoPublishAlias:
6869
DeploymentPreference:
6970
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Nested App Example
2+
3+
This app uses the [twitter event source app](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:077246666028:applications~aws-serverless-twitter-event-source) as a nested app and logs the tweets received from the nested app.
4+
5+
All you need to do is supply the desired parameters for this app and deploy. SAM will create a nested stack for any nested app inside of your template with all of the parameters that are passed to it.
6+
7+
## Installation Instructions
8+
Please refer to the Installation Steps section of the [twitter-event-source application](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:077246666028:applications~aws-serverless-twitter-event-source) for detailed information regarding how to obtain and use the tokens and secrets for this application.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import logging
2+
3+
LOGGER = logging.getLogger()
4+
LOGGER.setLevel(logging.INFO)
5+
6+
def process_tweets(tweets, context):
7+
LOGGER.info("Received tweets: {}".format(tweets))
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: 'AWS::Serverless-2016-10-31'
3+
Description: This example imports the aws-serverless-twitter-event-source serverless app as a nested app in this serverless application and connects it to a function that will log the tweets sent for the given Twitter search text.
4+
Parameters:
5+
EncryptedAccessToken:
6+
Type: String
7+
Description: Twitter API Access Token encrypted ciphertext blob as a base64-encoded string.
8+
EncryptedAccessTokenSecret:
9+
Type: String
10+
Description: Twitter API Access Token Secret ciphertext blob as a base64-encoded string.
11+
EncryptedConsumerKey:
12+
Type: String
13+
Description: Twitter API Consumer Key encrypted ciphertext blob as a base64-encoded string.
14+
EncryptedConsumerSecret:
15+
Type: String
16+
Description: Twitter API Consumer Secret encrypted ciphertext blob as a base64-encoded string.
17+
DecryptionKeyName:
18+
Type: String
19+
Description: KMS key name of the key used to encrypt the Twitter API parameters. Note, this must be just the key name (UUID), not the full key ARN. It's assumed the key is owned by the same account, in the same region as the app.
20+
SearchText:
21+
Type: String
22+
Description: Non-URL-encoded search text poller should use when querying Twitter Search API.
23+
Default: AWS
24+
25+
Resources:
26+
TweetLogger:
27+
Type: 'AWS::Serverless::Function'
28+
Properties:
29+
Handler: app.process_tweets
30+
Runtime: python3.6
31+
MemorySize: 128
32+
Timeout: 10
33+
CodeUri: src/
34+
TwitterEventSourceApp:
35+
Type: 'AWS::Serverless::Application'
36+
Properties:
37+
Location:
38+
ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/aws-serverless-twitter-event-source
39+
SemanticVersion: 1.1.0
40+
Parameters: # Using default value for PollingFrequencyInMinutes (1)
41+
TweetProcessorFunctionName: !Ref TweetLogger
42+
BatchSize: 20
43+
DecryptionKeyName: !Ref DecryptionKeyName
44+
EncryptedAccessToken: !Ref EncryptedAccessToken
45+
EncryptedAccessTokenSecret: !Ref EncryptedAccessTokenSecret
46+
EncryptedConsumerKey: !Ref EncryptedConsumerKey
47+
EncryptedConsumerSecret: !Ref EncryptedConsumerSecret
48+
SearchText: !Sub '${SearchText} -filter:nativeretweets' # filter out retweet records from search results
49+
TimeoutInMinutes: 20
50+
51+
Outputs:
52+
TweetProcessorFunctionArn:
53+
Value: !GetAtt TweetProcessorFunction.Arn
54+
TwitterSearchPollerFunctionArn:
55+
# Reference an output from the nested stack:
56+
Value: !GetAtt TwitterEventSourceApp.Outputs.TwitterSearchPollerFunctionArn

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.8.0'
1+
__version__ = '1.9.0'

samtranslator/intrinsics/actions.py

Lines changed: 170 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs):
3030
"""
3131
raise NotImplementedError("Subclass must implement this method")
3232

33+
def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs):
34+
"""
35+
Subclass must implement this method to resolve resource references
36+
"""
37+
raise NotImplementedError("Subclass must implement this method")
38+
3339
def can_handle(self, input_dict):
3440
"""
3541
Validates that the input dictionary contains only one key and is of the given intrinsic_name
@@ -127,6 +133,35 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs):
127133
self.intrinsic_name: resolved_value
128134
}
129135

136+
def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs):
137+
"""
138+
Updates references to the old logical id of a resource to the new (generated) logical id.
139+
140+
Example:
141+
{"Ref": "MyLayer"} => {"Ref": "MyLayerABC123"}
142+
143+
:param dict input_dict: Dictionary representing the Ref function to be resolved.
144+
:param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones.
145+
:return dict: Dictionary with resource references resolved.
146+
"""
147+
148+
if not self.can_handle(input_dict):
149+
return input_dict
150+
151+
ref_value = input_dict[self.intrinsic_name]
152+
if not isinstance(ref_value, string_types) or self._resource_ref_separator in ref_value:
153+
return input_dict
154+
155+
logical_id = ref_value
156+
157+
resolved_value = supported_resource_id_refs.get(logical_id)
158+
if not resolved_value:
159+
return input_dict
160+
161+
return {
162+
self.intrinsic_name: resolved_value
163+
}
164+
130165
class SubAction(Action):
131166
intrinsic_name = "Fn::Sub"
132167

@@ -140,11 +175,6 @@ def resolve_parameter_refs(self, input_dict, parameters):
140175
:param parameters: Dictionary of parameter values for substitution
141176
:return: Resolved
142177
"""
143-
if not self.can_handle(input_dict):
144-
return input_dict
145-
146-
key = self.intrinsic_name
147-
value = input_dict[key]
148178

149179
def do_replacement(full_ref, prop_name):
150180
"""
@@ -157,9 +187,8 @@ def do_replacement(full_ref, prop_name):
157187
"""
158188
return parameters.get(prop_name, full_ref)
159189

160-
input_dict[key] = self._handle_sub_value(value, do_replacement)
190+
return self._handle_sub_action(input_dict, do_replacement)
161191

162-
return input_dict
163192

164193
def resolve_resource_refs(self, input_dict, supported_resource_refs):
165194
"""
@@ -187,12 +216,6 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs):
187216
:return: Resolved dictionary
188217
"""
189218

190-
if not self.can_handle(input_dict):
191-
return input_dict
192-
193-
key = self.intrinsic_name
194-
sub_value = input_dict[key]
195-
196219
def do_replacement(full_ref, ref_value):
197220
"""
198221
Perform the appropriate replacement to handle ${LogicalId.Property} type references inside a Sub.
@@ -224,7 +247,83 @@ def do_replacement(full_ref, ref_value):
224247
replacement = self._resource_ref_separator.join([logical_id, property])
225248
return full_ref.replace(replacement, resolved_value)
226249

227-
input_dict[key] = self._handle_sub_value(sub_value, do_replacement)
250+
return self._handle_sub_action(input_dict, do_replacement)
251+
252+
253+
def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs):
254+
"""
255+
Resolves reference to some property of a resource. Inside string to be substituted, there could be either a
256+
"Ref" or a "GetAtt" usage of this property. They have to be handled differently.
257+
258+
Ref usages are directly converted to a Ref on the resolved value. GetAtt usages are split under the assumption
259+
that there can be only one property of resource referenced here. Everything else is an attribute reference.
260+
261+
Example:
262+
263+
Let's say `LogicalId` will be resolved to `NewLogicalId`
264+
265+
Ref usage:
266+
${LogicalId} => ${NewLogicalId}
267+
268+
GetAtt usage:
269+
${LogicalId.Arn} => ${NewLogicalId.Arn}
270+
${LogicalId.Attr1.Attr2} => {NewLogicalId.Attr1.Attr2}
271+
272+
273+
:param input_dict: Dictionary to be resolved
274+
:param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones.
275+
:return: Resolved dictionary
276+
"""
277+
278+
def do_replacement(full_ref, ref_value):
279+
"""
280+
Perform the appropriate replacement to handle ${LogicalId} type references inside a Sub.
281+
This method is called to get the replacement string for each reference within Sub's value
282+
283+
:param full_ref: Entire reference string such as "${LogicalId.Property}"
284+
:param ref_value: Just the value of the reference such as "LogicalId.Property"
285+
:return: Resolved reference of the structure "${SomeOtherLogicalId}". Result should always include the
286+
${} structure since we are not resolving to final value, but just converting one reference to another
287+
"""
288+
289+
# Split the value by separator, expecting to separate out LogicalId
290+
splits = ref_value.split(self._resource_ref_separator)
291+
292+
# If we don't find at least one part, there is nothing to resolve
293+
if len(splits) < 1:
294+
return full_ref
295+
296+
logical_id = splits[0]
297+
resolved_value = supported_resource_id_refs.get(logical_id)
298+
if not resolved_value:
299+
# This ID/property combination is not in the supported references
300+
return full_ref
301+
302+
# We found a LogicalId.Property combination that can be resolved. Construct the output by replacing
303+
# the part of the reference string and not constructing a new ref. This allows us to support GetAtt-like
304+
# syntax and retain other attributes. Ex: ${LogicalId.Property.Arn} => ${SomeOtherLogicalId.Arn}
305+
return full_ref.replace(logical_id, resolved_value)
306+
307+
return self._handle_sub_action(input_dict, do_replacement)
308+
309+
310+
def _handle_sub_action(self, input_dict, handler):
311+
"""
312+
Handles resolving replacements in the Sub action based on the handler that is passed as an input.
313+
314+
:param input_dict: Dictionary to be resolved
315+
:param supported_values: One of several different objects that contain the supported values that need to be changed.
316+
See each method above for specifics on these objects.
317+
:param handler: handler that is specific to each implementation.
318+
:return: Resolved value of the Sub dictionary
319+
"""
320+
if not self.can_handle(input_dict):
321+
return input_dict
322+
323+
key = self.intrinsic_name
324+
sub_value = input_dict[key]
325+
326+
input_dict[key] = self._handle_sub_value(sub_value, handler)
228327

229328
return input_dict
230329

@@ -345,9 +444,65 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs):
345444
remaining = splits[2:] # if any
346445

347446
resolved_value = supported_resource_refs.get(logical_id, property)
447+
return self._get_resolved_dictionary(input_dict, key, resolved_value, remaining)
448+
449+
450+
def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs):
451+
"""
452+
Resolve resource references within a GetAtt dict.
453+
454+
Example:
455+
{ "Fn::GetAtt": ["LogicalId", "Arn"] } => {"Fn::GetAtt": ["ResolvedLogicalId", "Arn"]}
456+
457+
458+
Theoretically, only the first element of the array can contain reference to SAM resources. The second element
459+
is name of an attribute (like Arn) of the resource.
460+
461+
However tools like AWS CLI apply the assumption that first element of the array is a LogicalId and cannot
462+
contain a 'dot'. So they break at the first dot to convert YAML tag to JSON map like this:
463+
464+
`!GetAtt LogicalId.Arn` => {"Fn::GetAtt": [ "LogicalId", "Arn" ] }
465+
466+
Therefore to resolve the reference, we join the array into a string, break it back up to check if it contains
467+
a known reference, and resolve it if we can.
468+
469+
:param input_dict: Dictionary to be resolved
470+
:param dict supported_resource_id_refs: Dictionary that maps old logical ids to new ones.
471+
:return: Resolved dictionary
472+
"""
473+
474+
if not self.can_handle(input_dict):
475+
return input_dict
476+
477+
key = self.intrinsic_name
478+
value = input_dict[key]
479+
480+
# Value must be an array with *at least* two elements. If not, this is invalid GetAtt syntax. We just pass along
481+
# the input to CFN for it to do the "official" validation.
482+
if not isinstance(value, list) or len(value) < 2:
483+
return input_dict
484+
485+
value_str = self._resource_ref_separator.join(value)
486+
splits = value_str.split(self._resource_ref_separator)
487+
logical_id = splits[0]
488+
remaining = splits[1:] # if any
489+
490+
resolved_value = supported_resource_id_refs.get(logical_id)
491+
return self._get_resolved_dictionary(input_dict, key, resolved_value, remaining)
492+
493+
494+
def _get_resolved_dictionary(self, input_dict, key, resolved_value, remaining):
495+
"""
496+
Resolves the function and returns the updated dictionary
497+
498+
:param input_dict: Dictionary to be resolved
499+
:param key: Name of this intrinsic.
500+
:param resolved_value: Resolved or updated value for this action.
501+
:param remaining: Remaining sections for the GetAtt action.
502+
"""
348503
if resolved_value:
349504
# We resolved to a new resource logicalId. Use this as the first element and keep remaining elements intact
350505
# This is the new value of Fn::GetAtt
351506
input_dict[key] = [resolved_value] + remaining
352507

353-
return input_dict
508+
return input_dict

0 commit comments

Comments
 (0)