Skip to content
This repository was archived by the owner on Aug 31, 2021. It is now read-only.

Commit e587a0b

Browse files
authored
fix: Strip app metadata in templates sent to SAR (#20)
* Strip app metadata in templates sent to SAR * Accept passing template as dictionary to publish_application and update_application_metadata * Use OrderedDict for json to preserve elements order
1 parent d2c7722 commit e587a0b

File tree

6 files changed

+222
-51
lines changed

6 files changed

+222
-51
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ For example:
2828

2929
```python
3030
import boto3
31+
import yaml
3132
from serverlessrepo import publish_application
3233

3334
sar_client = boto3.client('serverlessrepo', region_name='us-east-1')
@@ -36,7 +37,10 @@ with open('template.yaml', 'r') as f:
3637
template = f.read()
3738
# if sar_client is not provided, we will initiate the client using region inferred from aws configurations
3839
output = publish_application(template, sar_client)
39-
print (output)
40+
41+
# Alternatively, pass parsed template as a dictionary
42+
template_dict = yaml.loads(template)
43+
output = publish_application(template_dict, sar_client)
4044
```
4145

4246
The output of `publish_application` has the following structure:
@@ -75,6 +79,7 @@ For example:
7579

7680
```python
7781
import boto3
82+
import yaml
7883
from serverlessrepo import update_application_metadata
7984

8085
sar_client = boto3.client('serverlessrepo', region_name='us-east-1')
@@ -84,6 +89,10 @@ with open('template.yaml', 'r') as f:
8489
application_id = 'arn:aws:serverlessrepo:us-east-1:123456789012:applications/test-app'
8590
# if sar_client is not provided, we will initiate the client using region inferred from aws configurations
8691
update_application_metadata(template, application_id, sar_client)
92+
93+
# Alternatively, pass parsed template as a dictionary
94+
template_dict = yaml.loads(template)
95+
update_application_metadata(template_dict, application_id, sar_client)
8796
```
8897

8998
### Manage Application Permissions

serverlessrepo/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Serverlessrepo version and package meta-data."""
22

33
__title__ = 'serverlessrepo'
4-
__version__ = '0.1.4'
4+
__version__ = '0.1.5'
55
__license__ = 'Apache 2.0'
66
__description__ = (
77
'A Python library with convenience helpers for working '

serverlessrepo/parser.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Helper to parse JSON/YAML SAM template and dump YAML files."""
22

33
import re
4+
import copy
45
import json
56
from collections import OrderedDict
7+
68
import six
79
import yaml
810
from yaml.resolver import ScalarNode, SequenceNode
@@ -86,7 +88,7 @@ def parse_template(template_str):
8688
# PyYAML doesn't support json as well as it should, so if the input
8789
# is actually just json it is better to parse it with the standard
8890
# json parser.
89-
return json.loads(template_str)
91+
return json.loads(template_str, object_pairs_hook=OrderedDict)
9092
except ValueError:
9193
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _dict_constructor)
9294
yaml.SafeLoader.add_multi_constructor('!', intrinsics_multi_constructor)
@@ -103,8 +105,8 @@ def get_app_metadata(template_dict):
103105
:rtype: ApplicationMetadata
104106
:raises ApplicationMetadataNotFoundError
105107
"""
106-
if METADATA in template_dict and SERVERLESS_REPO_APPLICATION in template_dict[METADATA]:
107-
app_metadata_dict = template_dict[METADATA][SERVERLESS_REPO_APPLICATION]
108+
if SERVERLESS_REPO_APPLICATION in template_dict.get(METADATA, {}):
109+
app_metadata_dict = template_dict.get(METADATA).get(SERVERLESS_REPO_APPLICATION)
108110
return ApplicationMetadata(app_metadata_dict)
109111

110112
raise ApplicationMetadataNotFoundError(
@@ -122,3 +124,26 @@ def parse_application_id(text):
122124
"""
123125
result = re.search(APPLICATION_ID_PATTERN, text)
124126
return result.group(0) if result else None
127+
128+
129+
def strip_app_metadata(template_dict):
130+
"""
131+
Strip the "AWS::ServerlessRepo::Application" metadata section from template.
132+
133+
:param template_dict: SAM template as a dictionary
134+
:type template_dict: dict
135+
:return: stripped template content
136+
:rtype: str
137+
"""
138+
if SERVERLESS_REPO_APPLICATION not in template_dict.get(METADATA, {}):
139+
return template_dict
140+
141+
template_dict_copy = copy.deepcopy(template_dict)
142+
143+
# strip the whole metadata section if SERVERLESS_REPO_APPLICATION is the only key in it
144+
if not [k for k in template_dict_copy.get(METADATA) if k != SERVERLESS_REPO_APPLICATION]:
145+
template_dict_copy.pop(METADATA, None)
146+
else:
147+
template_dict_copy.get(METADATA).pop(SERVERLESS_REPO_APPLICATION, None)
148+
149+
return template_dict_copy

serverlessrepo/publish.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
"""Module containing functions to publish or update application."""
22

33
import re
4+
import copy
5+
46
import boto3
57
from botocore.exceptions import ClientError
68

79
from .application_metadata import ApplicationMetadata
8-
from .parser import parse_template, get_app_metadata, parse_application_id
10+
from .parser import (
11+
yaml_dump, parse_template, get_app_metadata,
12+
parse_application_id, strip_app_metadata
13+
)
914
from .exceptions import S3PermissionsRequired
1015

1116
CREATE_APPLICATION = 'CREATE_APPLICATION'
@@ -17,8 +22,8 @@ def publish_application(template, sar_client=None):
1722
"""
1823
Create a new application or new application version in SAR.
1924
20-
:param template: A packaged YAML or JSON SAM template
21-
:type template: str
25+
:param template: Content of a packaged YAML or JSON SAM template
26+
:type template: str_or_dict
2227
:param sar_client: The boto3 client used to access SAR
2328
:type sar_client: boto3.client
2429
:return: Dictionary containing application id, actions taken, and updated details
@@ -28,13 +33,15 @@ def publish_application(template, sar_client=None):
2833
if not template:
2934
raise ValueError('Require SAM template to publish the application')
3035

31-
template_dict = parse_template(template)
32-
app_metadata = get_app_metadata(template_dict)
3336
if not sar_client:
3437
sar_client = boto3.client('serverlessrepo')
3538

39+
template_dict = _get_template_dict(template)
40+
app_metadata = get_app_metadata(template_dict)
41+
stripped_template_dict = strip_app_metadata(template_dict)
42+
stripped_template = yaml_dump(stripped_template_dict)
3643
try:
37-
request = _create_application_request(app_metadata, template)
44+
request = _create_application_request(app_metadata, stripped_template)
3845
response = sar_client.create_application(**request)
3946
application_id = response['ApplicationId']
4047
actions = [CREATE_APPLICATION]
@@ -55,7 +62,7 @@ def publish_application(template, sar_client=None):
5562
# Create application version if semantic version is specified
5663
if app_metadata.semantic_version:
5764
try:
58-
request = _create_application_version_request(app_metadata, application_id, template)
65+
request = _create_application_version_request(app_metadata, application_id, stripped_template)
5966
sar_client.create_application_version(**request)
6067
actions.append(CREATE_APPLICATION_VERSION)
6168
except ClientError as e:
@@ -73,8 +80,8 @@ def update_application_metadata(template, application_id, sar_client=None):
7380
"""
7481
Update the application metadata.
7582
76-
:param template: A packaged YAML or JSON SAM template
77-
:type template: str
83+
:param template: Content of a packaged YAML or JSON SAM template
84+
:type template: str_or_dict
7885
:param application_id: The Amazon Resource Name (ARN) of the application
7986
:type application_id: str
8087
:param sar_client: The boto3 client used to access SAR
@@ -87,12 +94,31 @@ def update_application_metadata(template, application_id, sar_client=None):
8794
if not sar_client:
8895
sar_client = boto3.client('serverlessrepo')
8996

90-
template_dict = parse_template(template)
97+
template_dict = _get_template_dict(template)
9198
app_metadata = get_app_metadata(template_dict)
9299
request = _update_application_request(app_metadata, application_id)
93100
sar_client.update_application(**request)
94101

95102

103+
def _get_template_dict(template):
104+
"""
105+
Parse string template and or copy dictionary template.
106+
107+
:param template: Content of a packaged YAML or JSON SAM template
108+
:type template: str_or_dict
109+
:return: Template as a dictionary
110+
:rtype: dict
111+
:raises ValueError
112+
"""
113+
if isinstance(template, str):
114+
return parse_template(template)
115+
116+
if isinstance(template, dict):
117+
return copy.deepcopy(template)
118+
119+
raise ValueError('Input template should be a string or dictionary')
120+
121+
96122
def _create_application_request(app_metadata, template):
97123
"""
98124
Construct the request body to create application.

tests/unit/test_parser.py

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from collections import OrderedDict
23
from unittest import TestCase
34

45
from serverlessrepo.exceptions import ApplicationMetadataNotFoundError
@@ -79,6 +80,34 @@ def test_parse_json_with_tabs(self):
7980
output = parser.parse_template(template)
8081
self.assertEqual(output, {'foo': 'bar'})
8182

83+
def test_parse_json_preserve_elements_order(self):
84+
input_template = """
85+
{
86+
"B_Resource": {
87+
"Key2": {
88+
"Name": "name2"
89+
},
90+
"Key1": {
91+
"Name": "name1"
92+
}
93+
},
94+
"A_Resource": {
95+
"Key2": {
96+
"Name": "name2"
97+
},
98+
"Key1": {
99+
"Name": "name1"
100+
}
101+
}
102+
}
103+
"""
104+
expected_dict = OrderedDict([
105+
('B_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})])),
106+
('A_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})]))
107+
])
108+
output_dict = parser.parse_template(input_template)
109+
self.assertEqual(expected_dict, output_dict)
110+
82111
def test_parse_yaml_preserve_elements_order(self):
83112
input_template = """
84113
B_Resource:
@@ -93,17 +122,12 @@ def test_parse_yaml_preserve_elements_order(self):
93122
Name: name1
94123
"""
95124
output_dict = parser.parse_template(input_template)
96-
expected_dict = {
97-
'B_Resource': {
98-
'Key2': {'Name': 'name2'},
99-
'Key1': {'Name': 'name1'}
100-
},
101-
'A_Resource': {
102-
'Key2': {'Name': 'name2'},
103-
'Key1': {'Name': 'name1'}
104-
}
105-
}
125+
expected_dict = OrderedDict([
126+
('B_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})])),
127+
('A_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})]))
128+
])
106129
self.assertEqual(expected_dict, output_dict)
130+
107131
output_template = parser.yaml_dump(output_dict)
108132
# yaml dump changes indentation, remove spaces and new line characters to just compare the text
109133
self.assertEqual(re.sub(r'\n|\s', '', input_template),
@@ -174,3 +198,36 @@ def test_parse_application_id_return_none(self):
174198
text_without_application_id = 'text without application id'
175199
result = parser.parse_application_id(text_without_application_id)
176200
self.assertIsNone(result)
201+
202+
def test_strip_app_metadata_when_input_does_not_contain_metadata(self):
203+
template_dict = {'Resources': {}}
204+
actual_output = parser.strip_app_metadata(template_dict)
205+
self.assertEqual(actual_output, template_dict)
206+
207+
def test_strip_app_metadata_when_metadata_only_contains_app_metadata(self):
208+
template_dict = {
209+
'Metadata': {
210+
'AWS::ServerlessRepo::Application': {}
211+
},
212+
'Resources': {},
213+
}
214+
expected_output = {'Resources': {}}
215+
actual_output = parser.strip_app_metadata(template_dict)
216+
self.assertEqual(actual_output, expected_output)
217+
218+
def test_strip_app_metadata_when_metadata_contains_additional_keys(self):
219+
template_dict = {
220+
'Metadata': {
221+
'AWS::ServerlessRepo::Application': {},
222+
'AnotherKey': {}
223+
},
224+
'Resources': {}
225+
}
226+
expected_output = {
227+
'Metadata': {
228+
'AnotherKey': {}
229+
},
230+
'Resources': {}
231+
}
232+
actual_output = parser.strip_app_metadata(template_dict)
233+
self.assertEqual(actual_output, expected_output)

0 commit comments

Comments
 (0)