Skip to content
Merged
95 changes: 84 additions & 11 deletions bin/sam-translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@
Known limitations: cannot transform CodeUri pointing at local directory.

Usage:
sam-translate.py --input-file=sam-template.yaml [--output-file=<o>]
sam-translate.py --template-file=sam-template.yaml [--verbose] [--output-template=<o>]
sam-translate.py package --template-file=sam-template.yaml --s3-bucket=my-bucket [--verbose] [--output-template=<o>]
sam-translate.py deploy --template-file=sam-template.yaml --s3-bucket=my-bucket --capabilities=CAPABILITY_NAMED_IAM --stack-name=my-stack [--verbose] [--output-template=<o>]

Options:
--input-file=<i> Location of SAM template to transform.
--output-file=<o> Location to store resulting CloudFormation template [default: cfn-template.json].
--template-file=<i> Location of SAM template to transform [default: template.yaml].
--output-template=<o> Location to store resulting CloudFormation template [default: transformed-template.json].
--s3-bucket=<s> S3 bucket to use for SAM artifacts when using the `package` command
--capabilities=<c> Capabilities
--stack-name=<n> Unique name for your CloudFormation Stack
--verbose Enables verbose logging

"""
import json
import logging
import os
import platform
import subprocess
import sys

import boto3
from docopt import docopt
Expand All @@ -23,24 +33,60 @@
from samtranslator.yaml_helper import yaml_parse
from samtranslator.model.exceptions import InvalidDocumentException


LOG = logging.getLogger(__name__)
cli_options = docopt(__doc__)
iam_client = boto3.client('iam')
cwd = os.getcwd()

if cli_options.get('--verbose'):
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig()

def execute_command(command, args):
try:
aws_cmd = 'aws' if platform.system().lower() != 'windows' else 'aws.cmd'
command_with_args = [aws_cmd, 'cloudformation', command] + list(args)

LOG.debug("Executing command: %s", command_with_args)

subprocess.check_call(command_with_args)

LOG.debug("Command successful")
except subprocess.CalledProcessError as e:
# Underlying aws command will print the exception to the user
LOG.debug("Exception: %s", e)
sys.exit(e.returncode)


def get_input_output_file_paths():
input_file_option = cli_options.get('--input-file')
output_file_option = cli_options.get('--output-file')
input_file_option = cli_options.get('--template-file')
output_file_option = cli_options.get('--output-template')
input_file_path = os.path.join(cwd, input_file_option)
output_file_path = os.path.join(cwd, output_file_option)

return input_file_path, output_file_path


def main():
input_file_path, output_file_path = get_input_output_file_paths()
def package(input_file_path, output_file_path):
template_file = input_file_path
package_output_template_file = input_file_path + '._sam_packaged_.yaml'
s3_bucket = cli_options.get('--s3-bucket')
args = [
'--template-file',
template_file,
'--output-template-file',
package_output_template_file,
'--s3-bucket',
s3_bucket
]

execute_command('package', args)

return package_output_template_file


def transform_template(input_file_path, output_file_path):
with open(input_file_path, 'r') as f:
sam_template = yaml_parse(f)

Expand All @@ -56,10 +102,37 @@ def main():
print('Wrote transformed CloudFormation template to: ' + output_file_path)
except InvalidDocumentException as e:
errorMessage = reduce(lambda message, error: message + ' ' + error.message, e.causes, e.message)
print(errorMessage)
LOG.error(errorMessage)
errors = map(lambda cause: cause.message, e.causes)
print(errors)
LOG.error(errors)


def deploy(template_file):
capabilities = cli_options.get('--capabilities')
stack_name = cli_options.get('--stack-name')
args = [
'--template-file',
template_file,
'--capabilities',
capabilities,
'--stack-name',
stack_name
]

execute_command('deploy', args)

return package_output_template_file


if __name__ == '__main__':
main()
input_file_path, output_file_path = get_input_output_file_paths()

if cli_options.get('package'):
package_output_template_file = package(input_file_path, output_file_path)
transform_template(package_output_template_file, output_file_path)
elif cli_options.get('deploy'):
package_output_template_file = package(input_file_path, output_file_path)
transform_template(package_output_template_file, output_file_path)
deploy(output_file_path)
else:
transform_template(input_file_path, output_file_path)
18 changes: 16 additions & 2 deletions samtranslator/parser/parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException
from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException, InvalidResourceException
from samtranslator.validator.validator import SamTemplateValidator
from samtranslator.plugins import LifeCycleEvents
from samtranslator.public.sdk.template import SamTemplate


class Parser:
Expand All @@ -26,11 +27,24 @@ def _validate(self, sam_template, parameter_values):
raise InvalidDocumentException(
[InvalidTemplateException("'Resources' section is required")])

if (not all(isinstance(value, dict) for value in sam_template["Resources"].values())):
if (not all(isinstance(sam_resource, dict) for sam_resource in sam_template["Resources"].values())):
raise InvalidDocumentException(
[InvalidTemplateException(
"All 'Resources' must be Objects. If you're using YAML, this may be an "
"indentation issue."
)])

sam_template_instance = SamTemplate(sam_template)

for resource_logical_id, sam_resource in sam_template_instance.iterate():
# NOTE: Properties isn't required for SimpleTable, so we can't check
# `not isinstance(sam_resources.get("Properties"), dict)` as this would be a breaking change.
# sam_resource.properties defaults to {} in SamTemplate init
if (not isinstance(sam_resource.properties, dict)):
raise InvalidDocumentException(
[InvalidResourceException(resource_logical_id,
"All 'Resources' must be Objects and have a 'Properties' Object. If "
"you're using YAML, this may be an indentation issue."
)])

SamTemplateValidator.validate(sam_template)
22 changes: 21 additions & 1 deletion samtranslator/plugins/application/serverless_app_plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import boto3
import json
from botocore.exceptions import ClientError, EndpointConnectionError
import logging
from time import sleep, time
Expand Down Expand Up @@ -86,10 +87,17 @@ def on_before_transform_template(self, template_dict):

app_id = self._replace_value(app.properties[self.LOCATION_KEY],
self.APPLICATION_ID_KEY, intrinsic_resolvers)

semver = self._replace_value(app.properties[self.LOCATION_KEY],
self.SEMANTIC_VERSION_KEY, intrinsic_resolvers)

if isinstance(app_id, dict) or isinstance(semver, dict):
key = (json.dumps(app_id), json.dumps(semver))
self._applications[key] = False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose to set this to False?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just needed a value here. Wanted to get any other recommendations from you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. We're getting a value here because we don't resolve intrinsics until later, right? Could we resolve them earlier so that the flow in this area remains the same?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can actually stick an error instead of False into self._applications[key]. If the app_id or semver is still a dict, we won't resolve it any further so we'll just have to return an error at some point.

continue

key = (app_id, semver)

if key not in self._applications:
try:
# Lazy initialization of the client- create it when it is needed
Expand Down Expand Up @@ -211,11 +219,23 @@ def on_before_transform_resource(self, logical_id, resource_type, resource_prope
[self.APPLICATION_ID_KEY, self.SEMANTIC_VERSION_KEY])

app_id = resource_properties[self.LOCATION_KEY].get(self.APPLICATION_ID_KEY)

if not app_id:
raise InvalidResourceException(logical_id, "Property 'ApplicationId' cannot be blank.")

if isinstance(app_id, dict):
raise InvalidResourceException(logical_id, "Property 'ApplicationId' cannot be resolved. Only FindInMap "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages for both of these instances are practically the same, any way we could re-use the verbage and insert the property name?

"and Ref intrinsic functions are supported.")

semver = resource_properties[self.LOCATION_KEY].get(self.SEMANTIC_VERSION_KEY)

if not semver:
raise InvalidResourceException(logical_id, "Property 'SemanticVersion cannot be blank.")
raise InvalidResourceException(logical_id, "Property 'SemanticVersion' cannot be blank.")

if isinstance(semver, dict):
raise InvalidResourceException(logical_id, "Property 'SemanticVersion' cannot be resolved. Only FindInMap "
"and Ref intrinsic functions are supported.")

key = (app_id, semver)

# Throw any resource exceptions saved from the before_transform_template event
Expand Down
11 changes: 10 additions & 1 deletion tests/translator/input/error_application_properties.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ Resources:
Properties:
Location:
ApplicationId:
SemanticVersion:
SemanticVersion:

IntrinsicProperties:
Type: 'AWS::Serverless::Application'
Properties:
Location:
ApplicationId:
Sub: foobar
SemanticVersion:
Sub: foobar
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
4 changes: 2 additions & 2 deletions tests/translator/output/error_application_properties.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"errors": [
{
"errorMessage": "Resource with id [BlankProperties] is invalid. Property 'ApplicationId' cannot be blank. Resource with id [MissingApplicationId] is invalid. Resource is missing the required [ApplicationId] property. Resource with id [MissingLocation] is invalid. Resource is missing the required [Location] property. Resource with id [MissingSemanticVersion] is invalid. Resource is missing the required [SemanticVersion] property. Resource with id [NormalApplication] is invalid. Type of property 'ApplicationId' is invalid. Resource with id [UnsupportedProperty] is invalid. Resource is missing the required [Location] property."
"errorMessage": "Resource with id [BlankProperties] is invalid. Property 'ApplicationId' cannot be blank. Resource with id [IntrinsicProperties] is invalid. Property 'ApplicationId' cannot be resolved. Only FindInMap and Ref intrinsic functions are supported. Resource with id [MissingApplicationId] is invalid. Resource is missing the required [ApplicationId] property. Resource with id [MissingLocation] is invalid. Resource is missing the required [Location] property. Resource with id [MissingSemanticVersion] is invalid. Resource is missing the required [SemanticVersion] property. Resource with id [NormalApplication] is invalid. Type of property 'ApplicationId' is invalid. Resource with id [UnsupportedProperty] is invalid. Resource is missing the required [Location] property."
}
],
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [BlankProperties] is invalid. Property 'ApplicationId' cannot be blank. Resource with id [MissingApplicationId] is invalid. Resource is missing the required [ApplicationId] property. Resource with id [MissingLocation] is invalid. Resource is missing the required [Location] property. Resource with id [MissingSemanticVersion] is invalid. Resource is missing the required [SemanticVersion] property. Resource with id [NormalApplication] is invalid. Type of property 'ApplicationId' is invalid. Resource with id [UnsupportedProperty] is invalid. Resource is missing the required [Location] property."
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 7. Resource with id [BlankProperties] is invalid. Property 'ApplicationId' cannot be blank. Resource with id [IntrinsicProperties] is invalid. Property 'ApplicationId' cannot be resolved. Only FindInMap and Ref intrinsic functions are supported. Resource with id [MissingApplicationId] is invalid. Resource is missing the required [ApplicationId] property. Resource with id [MissingLocation] is invalid. Resource is missing the required [Location] property. Resource with id [MissingSemanticVersion] is invalid. Resource is missing the required [SemanticVersion] property. Resource with id [NormalApplication] is invalid. Type of property 'ApplicationId' is invalid. Resource with id [UnsupportedProperty] is invalid. Resource is missing the required [Location] property."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"errorMessage": "Resource with id [Function] is invalid. All 'Resources' must be Objects and have a 'Properties' Object. If you're using YAML, this may be an indentation issue."
}
],
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. All 'Resources' must be Objects and have a 'Properties' Object. If you're using YAML, this may be an indentation issue."
}
1 change: 1 addition & 0 deletions tests/translator/test_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_sw
'existing_role_logical_id',
'error_invalid_template',
'error_resource_not_dict',
'error_resource_properties_not_dict',
'error_globals_is_not_dict',
'error_globals_unsupported_type',
'error_globals_unsupported_property',
Expand Down