Skip to content

Commit bb1f9c8

Browse files
committed
Look Up Application ID Instead of Parsing It
Closes amazon-archives#34
1 parent b08cb94 commit bb1f9c8

File tree

6 files changed

+408
-331
lines changed

6 files changed

+408
-331
lines changed

.pylintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ confidence=
6262
# --disable=W".
6363
disable=useless-object-inheritance,
6464
unused-argument,
65-
too-many-instance-attributes
65+
too-many-instance-attributes,
66+
bad-continuation
6667

6768
# Enable the message, report, category or checker with the given id(s). You can
6869
# either give multiple identifier separated by comma (,) or put this option

serverlessrepo/exceptions.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
class ServerlessRepoError(Exception):
55
"""Base exception raised by serverlessrepo library."""
66

7-
MESSAGE = ''
7+
MESSAGE = ""
88

99
def __init__(self, **kwargs):
1010
"""Init the exception object."""
@@ -32,11 +32,13 @@ class InvalidApplicationPolicyError(ServerlessRepoError):
3232
class S3PermissionsRequired(ServerlessRepoError):
3333
"""Raised when S3 bucket access is denied."""
3434

35-
MESSAGE = "The AWS Serverless Application Repository does not have read access to bucket '{bucket}', " \
36-
"key '{key}'. Please update your Amazon S3 bucket policy to grant the service read " \
37-
"permissions to the application artifacts you have uploaded to your S3 bucket. See " \
38-
"https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverless-app-publishing-applications.html" \
39-
" for more details."
35+
MESSAGE = (
36+
"The AWS Serverless Application Repository does not have read access to bucket '{bucket}', "
37+
"key '{key}'. Please update your Amazon S3 bucket policy to grant the service read "
38+
"permissions to the application artifacts you have uploaded to your S3 bucket. See "
39+
"https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverless-app-publishing-applications.html"
40+
" for more details."
41+
)
4042

4143

4244
class InvalidS3UriError(ServerlessRepoError):
@@ -49,3 +51,9 @@ class ServerlessRepoClientError(ServerlessRepoError):
4951
"""Wrapper for botocore ClientError."""
5052

5153
MESSAGE = "{message}"
54+
55+
56+
class MultipleMatchingApplicationsError(ServerlessRepoError):
57+
"""Raised when multiple matching applications are found."""
58+
59+
MESSAGE = "{message}"

serverlessrepo/parser.py

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Helper to parse JSON/YAML SAM template and dump YAML files."""
22

3-
import re
43
import copy
54
import json
65
from collections import OrderedDict
@@ -12,9 +11,8 @@
1211
from .application_metadata import ApplicationMetadata
1312
from .exceptions import ApplicationMetadataNotFoundError
1413

15-
METADATA = 'Metadata'
16-
SERVERLESS_REPO_APPLICATION = 'AWS::ServerlessRepo::Application'
17-
APPLICATION_ID_PATTERN = r'arn:[\w\-]+:serverlessrepo:[\w\-]+:[0-9]+:applications\/[\S]+'
14+
METADATA = "Metadata"
15+
SERVERLESS_REPO_APPLICATION = "AWS::ServerlessRepo::Application"
1816

1917

2018
def intrinsics_multi_constructor(loader, tag_prefix, node):
@@ -27,17 +25,17 @@ def intrinsics_multi_constructor(loader, tag_prefix, node):
2725
tag = node.tag[1:]
2826

2927
# Some intrinsic functions doesn't support prefix "Fn::"
30-
prefix = 'Fn::'
31-
if tag in ['Ref', 'Condition']:
32-
prefix = ''
28+
prefix = "Fn::"
29+
if tag in ["Ref", "Condition"]:
30+
prefix = ""
3331

3432
cfntag = prefix + tag
3533

36-
if tag == 'GetAtt' and isinstance(node.value, six.string_types):
34+
if tag == "GetAtt" and isinstance(node.value, six.string_types):
3735
# ShortHand notation for !GetAtt accepts Resource.Attribute format
3836
# while the standard notation is to use an array
3937
# [Resource, Attribute]. Convert shorthand to standard format
40-
value = node.value.split('.', 1)
38+
value = node.value.split(".", 1)
4139

4240
elif isinstance(node, ScalarNode):
4341
# Value of this node is scalar
@@ -90,8 +88,10 @@ def parse_template(template_str):
9088
# json parser.
9189
return json.loads(template_str, object_pairs_hook=OrderedDict)
9290
except ValueError:
93-
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _dict_constructor)
94-
yaml.SafeLoader.add_multi_constructor('!', intrinsics_multi_constructor)
91+
yaml.SafeLoader.add_constructor(
92+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _dict_constructor
93+
)
94+
yaml.SafeLoader.add_multi_constructor("!", intrinsics_multi_constructor)
9595
return yaml.safe_load(template_str)
9696

9797

@@ -110,20 +110,10 @@ def get_app_metadata(template_dict):
110110
return ApplicationMetadata(app_metadata_dict)
111111

112112
raise ApplicationMetadataNotFoundError(
113-
error_message='missing {} section in template Metadata'.format(SERVERLESS_REPO_APPLICATION))
114-
115-
116-
def parse_application_id(text):
117-
"""
118-
Extract the application id from input text.
119-
120-
:param text: text to parse
121-
:type text: str
122-
:return: application id if found in the input
123-
:rtype: str
124-
"""
125-
result = re.search(APPLICATION_ID_PATTERN, text)
126-
return result.group(0) if result else None
113+
error_message="missing {} section in template Metadata".format(
114+
SERVERLESS_REPO_APPLICATION
115+
)
116+
)
127117

128118

129119
def strip_app_metadata(template_dict):
@@ -141,7 +131,8 @@ def strip_app_metadata(template_dict):
141131
template_dict_copy = copy.deepcopy(template_dict)
142132

143133
# 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]:
134+
metadata = template_dict_copy.get(METADATA)
135+
if not any(k for k in metadata if k != SERVERLESS_REPO_APPLICATION):
145136
template_dict_copy.pop(METADATA, None)
146137
else:
147138
template_dict_copy.get(METADATA).pop(SERVERLESS_REPO_APPLICATION, None)

serverlessrepo/publish.py

Lines changed: 83 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
from botocore.exceptions import ClientError
88

99
from .application_metadata import ApplicationMetadata
10-
from .parser import (
11-
yaml_dump, parse_template, get_app_metadata,
12-
parse_application_id, strip_app_metadata
10+
from .parser import yaml_dump, parse_template, get_app_metadata, strip_app_metadata
11+
from .exceptions import (
12+
MultipleMatchingApplicationsError,
13+
ServerlessRepoClientError,
14+
S3PermissionsRequired,
15+
InvalidS3UriError,
1316
)
14-
from .exceptions import ServerlessRepoClientError, S3PermissionsRequired, InvalidS3UriError
1517

16-
CREATE_APPLICATION = 'CREATE_APPLICATION'
17-
UPDATE_APPLICATION = 'UPDATE_APPLICATION'
18-
CREATE_APPLICATION_VERSION = 'CREATE_APPLICATION_VERSION'
18+
CREATE_APPLICATION = "CREATE_APPLICATION"
19+
UPDATE_APPLICATION = "UPDATE_APPLICATION"
20+
CREATE_APPLICATION_VERSION = "CREATE_APPLICATION_VERSION"
1921

2022

2123
def publish_application(template, sar_client=None):
@@ -31,10 +33,10 @@ def publish_application(template, sar_client=None):
3133
:raises ValueError
3234
"""
3335
if not template:
34-
raise ValueError('Require SAM template to publish the application')
36+
raise ValueError("Require SAM template to publish the application")
3537

3638
if not sar_client:
37-
sar_client = boto3.client('serverlessrepo')
39+
sar_client = boto3.client("serverlessrepo")
3840

3941
template_dict = _get_template_dict(template)
4042
app_metadata = get_app_metadata(template_dict)
@@ -43,15 +45,14 @@ def publish_application(template, sar_client=None):
4345
try:
4446
request = _create_application_request(app_metadata, stripped_template)
4547
response = sar_client.create_application(**request)
46-
application_id = response['ApplicationId']
48+
application_id = response["ApplicationId"]
4749
actions = [CREATE_APPLICATION]
4850
except ClientError as e:
4951
if not _is_conflict_exception(e):
5052
raise _wrap_client_error(e)
5153

5254
# Update the application if it already exists
53-
error_message = e.response['Error']['Message']
54-
application_id = parse_application_id(error_message)
55+
application_id = _get_application_id(sar_client, app_metadata)
5556
try:
5657
request = _update_application_request(app_metadata, application_id)
5758
sar_client.update_application(**request)
@@ -62,17 +63,19 @@ def publish_application(template, sar_client=None):
6263
# Create application version if semantic version is specified
6364
if app_metadata.semantic_version:
6465
try:
65-
request = _create_application_version_request(app_metadata, application_id, stripped_template)
66+
request = _create_application_version_request(
67+
app_metadata, application_id, stripped_template
68+
)
6669
sar_client.create_application_version(**request)
6770
actions.append(CREATE_APPLICATION_VERSION)
6871
except ClientError as e:
6972
if not _is_conflict_exception(e):
7073
raise _wrap_client_error(e)
7174

7275
return {
73-
'application_id': application_id,
74-
'actions': actions,
75-
'details': _get_publish_details(actions, app_metadata.template_dict)
76+
"application_id": application_id,
77+
"actions": actions,
78+
"details": _get_publish_details(actions, app_metadata.template_dict),
7679
}
7780

7881

@@ -89,10 +92,12 @@ def update_application_metadata(template, application_id, sar_client=None):
8992
:raises ValueError
9093
"""
9194
if not template or not application_id:
92-
raise ValueError('Require SAM template and application ID to update application metadata')
95+
raise ValueError(
96+
"Require SAM template and application ID to update application metadata"
97+
)
9398

9499
if not sar_client:
95-
sar_client = boto3.client('serverlessrepo')
100+
sar_client = boto3.client("serverlessrepo")
96101

97102
template_dict = _get_template_dict(template)
98103
app_metadata = get_app_metadata(template_dict)
@@ -116,7 +121,7 @@ def _get_template_dict(template):
116121
if isinstance(template, dict):
117122
return copy.deepcopy(template)
118123

119-
raise ValueError('Input template should be a string or dictionary')
124+
raise ValueError("Input template should be a string or dictionary")
120125

121126

122127
def _create_application_request(app_metadata, template):
@@ -130,21 +135,21 @@ def _create_application_request(app_metadata, template):
130135
:return: SAR CreateApplication request body
131136
:rtype: dict
132137
"""
133-
app_metadata.validate(['author', 'description', 'name'])
138+
app_metadata.validate(["author", "description", "name"])
134139
request = {
135-
'Author': app_metadata.author,
136-
'Description': app_metadata.description,
137-
'HomePageUrl': app_metadata.home_page_url,
138-
'Labels': app_metadata.labels,
139-
'LicenseBody': app_metadata.license_body,
140-
'LicenseUrl': app_metadata.license_url,
141-
'Name': app_metadata.name,
142-
'ReadmeBody': app_metadata.readme_body,
143-
'ReadmeUrl': app_metadata.readme_url,
144-
'SemanticVersion': app_metadata.semantic_version,
145-
'SourceCodeUrl': app_metadata.source_code_url,
146-
'SpdxLicenseId': app_metadata.spdx_license_id,
147-
'TemplateBody': template
140+
"Author": app_metadata.author,
141+
"Description": app_metadata.description,
142+
"HomePageUrl": app_metadata.home_page_url,
143+
"Labels": app_metadata.labels,
144+
"LicenseBody": app_metadata.license_body,
145+
"LicenseUrl": app_metadata.license_url,
146+
"Name": app_metadata.name,
147+
"ReadmeBody": app_metadata.readme_body,
148+
"ReadmeUrl": app_metadata.readme_url,
149+
"SemanticVersion": app_metadata.semantic_version,
150+
"SourceCodeUrl": app_metadata.source_code_url,
151+
"SpdxLicenseId": app_metadata.spdx_license_id,
152+
"TemplateBody": template,
148153
}
149154
# Remove None values
150155
return {k: v for k, v in request.items() if v}
@@ -162,13 +167,13 @@ def _update_application_request(app_metadata, application_id):
162167
:rtype: dict
163168
"""
164169
request = {
165-
'ApplicationId': application_id,
166-
'Author': app_metadata.author,
167-
'Description': app_metadata.description,
168-
'HomePageUrl': app_metadata.home_page_url,
169-
'Labels': app_metadata.labels,
170-
'ReadmeBody': app_metadata.readme_body,
171-
'ReadmeUrl': app_metadata.readme_url
170+
"ApplicationId": application_id,
171+
"Author": app_metadata.author,
172+
"Description": app_metadata.description,
173+
"HomePageUrl": app_metadata.home_page_url,
174+
"Labels": app_metadata.labels,
175+
"ReadmeBody": app_metadata.readme_body,
176+
"ReadmeUrl": app_metadata.readme_url,
172177
}
173178
return {k: v for k, v in request.items() if v}
174179

@@ -186,12 +191,12 @@ def _create_application_version_request(app_metadata, application_id, template):
186191
:return: SAR CreateApplicationVersion request body
187192
:rtype: dict
188193
"""
189-
app_metadata.validate(['semantic_version'])
194+
app_metadata.validate(["semantic_version"])
190195
request = {
191-
'ApplicationId': application_id,
192-
'SemanticVersion': app_metadata.semantic_version,
193-
'SourceCodeUrl': app_metadata.source_code_url,
194-
'TemplateBody': template
196+
"ApplicationId": application_id,
197+
"SemanticVersion": app_metadata.semantic_version,
198+
"SourceCodeUrl": app_metadata.source_code_url,
199+
"TemplateBody": template,
195200
}
196201
return {k: v for k, v in request.items() if v}
197202

@@ -204,8 +209,8 @@ def _is_conflict_exception(e):
204209
:type e: ClientError
205210
:return: True if e is ConflictException
206211
"""
207-
error_code = e.response['Error']['Code']
208-
return error_code == 'ConflictException'
212+
error_code = e.response["Error"]["Code"]
213+
return error_code == "ConflictException"
209214

210215

211216
def _wrap_client_error(e):
@@ -216,12 +221,12 @@ def _wrap_client_error(e):
216221
:type e: ClientError
217222
:return: S3PermissionsRequired or InvalidS3UriError or general ServerlessRepoClientError
218223
"""
219-
error_code = e.response['Error']['Code']
220-
message = e.response['Error']['Message']
224+
error_code = e.response["Error"]["Code"]
225+
message = e.response["Error"]["Message"]
221226

222-
if error_code == 'BadRequestException':
227+
if error_code == "BadRequestException":
223228
if "Failed to copy S3 object. Access denied:" in message:
224-
match = re.search('bucket=(.+?), key=(.+?)$', message)
229+
match = re.search("bucket=(.+?), key=(.+?)$", message)
225230
if match:
226231
return S3PermissionsRequired(bucket=match.group(1), key=match.group(2))
227232
if "Invalid S3 URI" in message:
@@ -250,11 +255,36 @@ def _get_publish_details(actions, app_metadata_template):
250255
ApplicationMetadata.HOME_PAGE_URL,
251256
ApplicationMetadata.LABELS,
252257
ApplicationMetadata.README_URL,
253-
ApplicationMetadata.README_BODY
258+
ApplicationMetadata.README_BODY,
254259
]
255260

256261
if CREATE_APPLICATION_VERSION in actions:
257262
# SemanticVersion and SourceCodeUrl can only be updated by creating a new version
258-
additional_keys = [ApplicationMetadata.SEMANTIC_VERSION, ApplicationMetadata.SOURCE_CODE_URL]
263+
additional_keys = [
264+
ApplicationMetadata.SEMANTIC_VERSION,
265+
ApplicationMetadata.SOURCE_CODE_URL,
266+
]
259267
include_keys.extend(additional_keys)
260268
return {k: v for k, v in app_metadata_template.items() if k in include_keys and v}
269+
270+
271+
def _get_application_id(sar_client, metadata):
272+
"""
273+
Gets the application ID of rhte matching application name.
274+
275+
:param sar_client: The boto3 SAR client.
276+
:param metadata: The application meta data.
277+
:return: The matching application ID.
278+
:rtype: str
279+
:raises: MultipleMatchingApplicationsError
280+
"""
281+
application_ids = []
282+
pager = sar_client.get_paginator("list_applications")
283+
for application in pager.paginate():
284+
if application["Name"] == metadata.name:
285+
application_ids.append(application["ApplicationId"])
286+
if len(application_ids) > 1:
287+
raise MultipleMatchingApplicationsError(
288+
message='Multiple applications with the name "%s"' % metadata.name
289+
)
290+
return application_ids[0] if len(application_ids) == 1 else None

0 commit comments

Comments
 (0)