Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 87 additions & 11 deletions bin/sam-translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,91 @@
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

my_path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, my_path + '/..')

from samtranslator.public.translator import ManagedPolicyLoader
from samtranslator.translator.transform import transform
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 +105,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)
10 changes: 0 additions & 10 deletions examples/2016-10-31/api_gateway_responses/src/index.js

This file was deleted.

31 changes: 8 additions & 23 deletions examples/2016-10-31/api_gateway_responses/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,31 @@ Transform: AWS::Serverless-2016-10-31
Description: Simple webservice deomnstrating gateway responses.

Resources:
ExplicitApi:
MyApi:
Type: AWS::Serverless::Api
Properties:
Auth:
Authorizers:
Authorizer:
FunctionArn: !GetAtt AuthorizerFunction.Arn
Identity:
ValidationExpression: "^Bearer +[-0-9a-zA-Z\\._]*$"
ReauthorizeEvery: 300
StageName: Prod
GatewayResponses:
UNAUTHORIZED:
DEFAULT_4xx:
ResponseParameters:
Headers:
Access-Control-Expose-Headers: "'WWW-Authenticate'"
Access-Control-Allow-Origin: "'*'"
WWW-Authenticate: >-
'Bearer realm="admin"'

GetFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.get
Runtime: nodejs6.10
CodeUri: src/
InlineCode: module.exports = async () => throw new Error('Check out the response headers!')
Events:
GetResource:
Type: Api
Properties:
Path: /resource/{resourceId}
Path: /error
Method: get
Auth:
Authorizer: Authorizer
RestApiId: !Ref ExplicitApi
AuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.auth
Runtime: nodejs6.10
CodeUri: src/
RestApiId: !Ref MyApi
Outputs:
ApiURL:
Description: "API endpoint URL for Prod environment"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/resource/"
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/error/"
4 changes: 2 additions & 2 deletions examples/2016-10-31/api_swagger_cors/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ paths:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations"

passthroughBehavior: when_no_match
httpMethod: POST
httpMethod: POST # Keep "POST" when the API definition method is not POST. This "httpMethod" is used to call Lambda.
type: aws_proxy
/{proxy+}:
x-amazon-apigateway-any-method:
Expand All @@ -41,7 +41,7 @@ paths:
x-amazon-apigateway-integration:
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations"
httpMethod: POST
httpMethod: POST # Keep "POST" when the API definition method is not POST. This "httpMethod" is used to call Lambda.
type: aws_proxy
definitions:
Empty:
Expand Down
12 changes: 9 additions & 3 deletions samtranslator/intrinsics/actions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re

from six import string_types
from samtranslator.model.exceptions import InvalidTemplateException
from samtranslator.model.exceptions import InvalidTemplateException, InvalidDocumentException


class Action(object):
Expand Down Expand Up @@ -427,6 +427,11 @@ def resolve_resource_refs(self, input_dict, supported_resource_refs):
if not isinstance(value, list) or len(value) < 2:
return input_dict

if (not all(isinstance(entry, string_types) for entry in value)):
raise InvalidDocumentException(
[InvalidTemplateException('Invalid GetAtt value {}. GetAtt expects an array with 2 strings.'
.format(value))])

# Value of GetAtt is an array. It can contain any number of elements, with first being the LogicalId of
# resource and rest being the attributes. In a SAM template, a reference to a resource can be used in the
# first parameter. However tools like AWS CLI might break them down as well. So let's just concatenate
Expand Down Expand Up @@ -529,8 +534,9 @@ def resolve_parameter_refs(self, input_dict, parameters):

# FindInMap expects an array with 3 values
if not isinstance(value, list) or len(value) != 3:
raise InvalidTemplateException('Invalid FindInMap value {}. FindInMap expects an array with 3 values.'
.format(value))
raise InvalidDocumentException(
[InvalidTemplateException('Invalid FindInMap value {}. FindInMap expects an array with 3 values.'
.format(value))])

map_name = self.resolve_parameter_refs(value[0], parameters)
top_level_key = self.resolve_parameter_refs(value[1], parameters)
Expand Down
4 changes: 3 additions & 1 deletion samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from samtranslator.model.events import EventsRule
from samtranslator.model.iot import IotTopicRule
from samtranslator.translator.arn_generator import ArnGenerator
from samtranslator.model.exceptions import InvalidEventException
from samtranslator.model.exceptions import InvalidEventException, InvalidResourceException
from samtranslator.swagger.swagger import SwaggerEditor

CONDITION = 'Condition'
Expand Down Expand Up @@ -419,6 +419,8 @@ def resources_to_link(self, resources):

# Stage could be a intrinsic, in which case leave the suffix to default value
if isinstance(permitted_stage, string_types):
if not permitted_stage:
raise InvalidResourceException(rest_api_id, 'StageName cannot be empty.')
stage_suffix = permitted_stage
else:
stage_suffix = "Stage"
Expand Down
2 changes: 1 addition & 1 deletion samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ def _construct_alias(self, name, function, version):
"""

if not name:
raise ValueError("Alias name is required to create an alias")
raise InvalidResourceException(self.logical_id, "Alias name is required to create an alias")

logical_id = "{id}Alias{suffix}".format(id=function.logical_id, suffix=name)
alias = LambdaAlias(logical_id=logical_id, attributes=self.get_passthrough_resource_attributes())
Expand Down
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
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 "
"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
5 changes: 4 additions & 1 deletion samtranslator/swagger/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from samtranslator.model.intrinsics import ref
from samtranslator.model.intrinsics import make_conditional
from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException


class SwaggerEditor(object):
Expand Down Expand Up @@ -124,7 +125,9 @@ def add_path(self, path, method=None):

if not isinstance(path_dict, dict):
# Either customers has provided us an invalid Swagger, or this class has messed it somehow
raise ValueError("Value of '{}' path must be a dictionary according to Swagger spec".format(path))
raise InvalidDocumentException(
[InvalidTemplateException("Value of '{}' path must be a dictionary according to Swagger spec."
.format(path))])

if self._CONDITIONAL_IF in path_dict:
path_dict = path_dict[self._CONDITIONAL_IF][1]
Expand Down
Loading