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
24 changes: 21 additions & 3 deletions designs/_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,32 @@ used for and when do you clean up?**

**How do you validate new .samrc configuration?**

What is your Testing Plan (QA)?
===============================

Goal
----

Pre-requesites
--------------

Test Scenarios/Cases
--------------------

Expected Results
----------------

Pass/Fail
---------

Documentation Changes
---------------------
=====================

Open Issues
-----------
============

Task Breakdown
--------------
==============

- \[x\] Send a Pull Request with this design document
- \[ \] Build the command line interface
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ docker>=3.3.0
dateparser~=0.7
python-dateutil~=2.6
pathlib2~=2.3.2; python_version<"3.4"
requests==2.20.1
requests==2.22.0
serverlessrepo==0.1.8
aws_lambda_builders==0.3.0
2 changes: 1 addition & 1 deletion samcli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
SAM CLI version
"""

__version__ = '0.16.1'
__version__ = '0.17.0'
2 changes: 1 addition & 1 deletion samcli/commands/deploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
HELP_TEXT = """The sam deploy command creates a Cloudformation Stack and deploys your resources.

\b
e.g. sam deploy sam deploy --template-file packaged.yaml --stack-name sam-app --capabilities CAPABILITY_IAM
e.g. sam deploy --template-file packaged.yaml --stack-name sam-app --capabilities CAPABILITY_IAM

\b
This is an alias for aws cloudformation deploy. To learn about other parameters you can use,
Expand Down
7 changes: 6 additions & 1 deletion samcli/commands/local/cli_common/invoke_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def __init__(self, # pylint: disable=R0914
parameter_overrides=None,
layer_cache_basedir=None,
force_image_build=None,
aws_region=None):
aws_region=None,
aws_profile=None,
):
"""
Initialize the context

Expand Down Expand Up @@ -109,6 +111,7 @@ def __init__(self, # pylint: disable=R0914
self._layer_cache_basedir = layer_cache_basedir
self._force_image_build = force_image_build
self._aws_region = aws_region
self._aws_profile = aws_profile

self._template_dict = None
self._function_provider = None
Expand Down Expand Up @@ -197,6 +200,8 @@ def local_lambda_runner(self):
return LocalLambdaRunner(local_runtime=lambda_runtime,
function_provider=self._function_provider,
cwd=self.get_cwd(),
aws_profile=self._aws_profile,
aws_region=self._aws_region,
env_vars_values=self._env_vars_value,
debug_context=self._debug_context)

Expand Down
3 changes: 2 additions & 1 deletion samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def do_cli(ctx, function_identifier, template, event, no_event, env_vars, debug_
parameter_overrides=parameter_overrides,
layer_cache_basedir=layer_cache_basedir,
force_image_build=force_image_build,
aws_region=ctx.region) as context:
aws_region=ctx.region,
aws_profile=ctx.profile) as context:

# Invoke the function
context.local_lambda_runner.invoke(context.function_name,
Expand Down
15 changes: 8 additions & 7 deletions samcli/commands/local/lib/local_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def __init__(self,
local_runtime,
function_provider,
cwd,
aws_profile=None,
aws_region=None,
env_vars_values=None,
debug_context=None):
"""
Expand All @@ -43,6 +45,8 @@ def __init__(self,
self.local_runtime = local_runtime
self.provider = function_provider
self.cwd = cwd
self.aws_profile = aws_profile
self.aws_region = aws_region
self.env_vars_values = env_vars_values or {}
self.debug_context = debug_context

Expand Down Expand Up @@ -203,13 +207,10 @@ def get_aws_creds(self):
result = {}

# to pass command line arguments for region & profile to setup boto3 default session
if boto3.DEFAULT_SESSION:
session = boto3.DEFAULT_SESSION
else:
session = boto3.session.Session()

profile_name = session.profile_name if session else None
LOG.debug("Loading AWS credentials from session with profile '%s'", profile_name)
LOG.debug("Loading AWS credentials from session with profile '%s'", self.aws_profile)
# boto3.session.Session is not thread safe. To ensure we do not run into a race condition with start-lambda
# or start-api, we create the session object here on every invoke.
session = boto3.session.Session(profile_name=self.aws_profile, region_name=self.aws_region)

if not session:
return result
Expand Down
3 changes: 2 additions & 1 deletion samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def do_cli(ctx, host, port, static_dir, template, env_vars, debug_port, debug_ar
parameter_overrides=parameter_overrides,
layer_cache_basedir=layer_cache_basedir,
force_image_build=force_image_build,
aws_region=ctx.region) as invoke_context:
aws_region=ctx.region,
aws_profile=ctx.profile) as invoke_context:

service = LocalApiService(lambda_invoke_context=invoke_context,
port=port,
Expand Down
3 changes: 2 additions & 1 deletion samcli/commands/local/start_lambda/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ def do_cli(ctx, host, port, template, env_vars, debug_port, debug_args, # pylin
parameter_overrides=parameter_overrides,
layer_cache_basedir=layer_cache_basedir,
force_image_build=force_image_build,
aws_region=ctx.region) as invoke_context:
aws_region=ctx.region,
aws_profile=ctx.profile) as invoke_context:

service = LocalLambdaService(lambda_invoke_context=invoke_context,
port=port,
Expand Down
46 changes: 41 additions & 5 deletions samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import base64

from flask import Flask, request
from werkzeug.datastructures import Headers

from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict
from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser
from samcli.lib.utils.stream_writer import StreamWriter
from samcli.local.lambdafn.exceptions import FunctionNotFound
from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent
Expand Down Expand Up @@ -165,7 +166,7 @@ def _request_handler(self, **kwargs):
route.binary_types,
request)
except (KeyError, TypeError, ValueError):
LOG.error("Function returned an invalid response (must include one of: body, headers or "
LOG.error("Function returned an invalid response (must include one of: body, headers, multiValueHeaders or "
"statusCode in the response object). Response received: %s", lambda_response)
return ServiceErrorResponses.lambda_failure_response()

Expand Down Expand Up @@ -207,7 +208,8 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request):
raise TypeError("Lambda returned %{s} instead of dict", type(json_output))

status_code = json_output.get("statusCode") or 200
headers = CaseInsensitiveDict(json_output.get("headers") or {})
headers = LocalApigwService._merge_response_headers(json_output.get("headers") or {},
json_output.get("multiValueHeaders") or {})
body = json_output.get("body") or "no data"
is_base_64_encoded = json_output.get("isBase64Encoded") or False

Expand Down Expand Up @@ -244,6 +246,7 @@ def _invalid_apig_response_keys(output):
"statusCode",
"body",
"headers",
"multiValueHeaders",
"isBase64Encoded"
}
# In Python 2.7, need to explicitly make the Dictionary keys into a set
Expand All @@ -261,7 +264,7 @@ def _should_base64_decode_body(binary_types, flask_request, lamba_response_heade
Corresponds to self.binary_types (aka. what is parsed from SAM Template
flask_request flask.request
Flask request
lamba_response_headers dict
lamba_response_headers werkzeug.datastructures.Headers
Headers Lambda returns
is_base_64_encoded bool
True if the body is Base64 encoded
Expand All @@ -271,11 +274,44 @@ def _should_base64_decode_body(binary_types, flask_request, lamba_response_heade
True if the body from the request should be converted to binary, otherwise false

"""
best_match_mimetype = flask_request.accept_mimetypes.best_match([lamba_response_headers["Content-Type"]])
best_match_mimetype = flask_request.accept_mimetypes.best_match(lamba_response_headers.get_all("Content-Type"))
is_best_match_in_binary_types = best_match_mimetype in binary_types or '*/*' in binary_types

return best_match_mimetype and is_best_match_in_binary_types and is_base_64_encoded

@staticmethod
def _merge_response_headers(headers, multi_headers):
"""
Merge multiValueHeaders headers with headers

* If you specify values for both headers and multiValueHeaders, API Gateway merges them into a single list.
* If the same key-value pair is specified in both, the value will only appear once.

Parameters
----------
headers dict
Headers map from the lambda_response_headers
multi_headers dict
multiValueHeaders map from the lambda_response_headers

Returns
-------
Merged list in accordance to the AWS documentation within a Flask Headers object

"""

processed_headers = Headers(multi_headers)

for header in headers:
# Prevent duplication of values when the key-value pair exists in both
# headers and multi_headers, but preserve order from multi_headers
if header in multi_headers and headers[header] in multi_headers[header]:
continue

processed_headers.add(header, headers[header])

return processed_headers

@staticmethod
def _construct_event(flask_request, port, binary_types):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This is a sample template for {{ cookiecutter.project_name }} - Below is a brief
.
├── README.MD <-- This instructions file
├── event.json <-- API Gateway Proxy Integration event payload
├── hello_world <-- Source code for a lambda function
├── hello-world <-- Source code for a lambda function
│ └── app.js <-- Lambda function code
│ └── package.json <-- NodeJS dependencies and scripts
│ └── tests <-- Unit tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This is a sample template for {{ cookiecutter.project_name }} - Below is a brief
.
├── README.MD <-- This instructions file
├── event.json <-- API Gateway Proxy Integration event payload
├── hello_world <-- Source code for a lambda function
├── hello-world <-- Source code for a lambda function
│ └── app.js <-- Lambda function code
│ └── package.json <-- NodeJS dependencies and scripts
│ └── app-deps.js <-- Lambda function code with dependencies (Bringing to the next level section)
Expand Down
4 changes: 2 additions & 2 deletions samcli/local/lambda_service/local_lambda_invoke_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask import Flask, request

from samcli.lib.utils.stream_writer import StreamWriter
from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict
from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser
from samcli.local.lambdafn.exceptions import FunctionNotFound
from .lambda_error_responses import LambdaErrorResponses

Expand Down Expand Up @@ -92,7 +92,7 @@ def validate_request():
LOG.debug("Query parameters are in the request but not supported")
return LambdaErrorResponses.invalid_request_content("Query Parameters are not supported")

request_headers = CaseInsensitiveDict(flask_request.headers)
request_headers = flask_request.headers

log_type = request_headers.get('X-Amz-Log-Type', 'None')
if log_type != 'None':
Expand Down
7 changes: 6 additions & 1 deletion samcli/local/layers/layer_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ def __init__(self, layer_cache, cwd, lambda_client=None):
"""
self._layer_cache = layer_cache
self.cwd = cwd
self.lambda_client = lambda_client or boto3.client('lambda')
self._lambda_client = lambda_client

@property
def lambda_client(self):
self._lambda_client = self._lambda_client or boto3.client('lambda')
return self._lambda_client

@property
def layer_cache(self):
Expand Down
19 changes: 1 addition & 18 deletions samcli/local/services/base_local_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,6 @@
LOG = logging.getLogger(__name__)


class CaseInsensitiveDict(dict):
"""
Implement a simple case insensitive dictionary for storing headers. To preserve the original
case of the given Header (e.g. X-FooBar-Fizz) this only touches the get and contains magic
methods rather than implementing a __setitem__ where we normalize the case of the headers.
"""

def __getitem__(self, key):
matches = [v for k, v in self.items() if k.lower() == key.lower()]
if not matches:
raise KeyError(key)
return matches[0]

def __contains__(self, key):
return key.lower() in [k.lower() for k in self.keys()]


class BaseLocalService(object):

def __init__(self, is_debugging, port, host):
Expand Down Expand Up @@ -86,7 +69,7 @@ def service_response(body, headers, status_code):
Constructs a Flask Response from the body, headers, and status_code.

:param str body: Response body as a string
:param dict headers: headers for the response
:param werkzeug.datastructures.Headers headers: headers for the response
:param int status_code: status_code for response
:return: Flask Response
"""
Expand Down
42 changes: 40 additions & 2 deletions tests/functional/local/apigw/test_local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,46 @@ def make_service(list_of_routes, function_provider, cwd):

def make_service_response(port, method, scheme, resourcePath, resolvedResourcePath, pathParameters=None,
body=None, headers=None, queryParams=None, isBase64Encoded=False):
response_str = '{"httpMethod": "GET", "body": null, "resource": "/something/{event}", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/something/{event}", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/something/{event}"}, "queryStringParameters": null, "headers": {"Host": "0.0.0.0:33651", "User-Agent": "python-requests/2.20.1", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive"}, "pathParameters": {"event": "event1"}, "stageVariables": null, "path": "/something/event1", "isBase64Encoded": false}' # NOQA
response = json.loads(response_str)
response = {
"httpMethod": "GET",
"body": None,
"resource": "/something/{event}",
"requestContext": {
"resourceId": "123456",
"apiId": "1234567890",
"resourcePath": "/something/{event}",
"httpMethod": "GET",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"accountId": "123456789012",
"stage": "prod",
"identity": {
"apiKey": None,
"userArn": None,
"cognitoAuthenticationType": None,
"caller": None,
"userAgent": "Custom User Agent String",
"user": None,
"cognitoIdentityPoolId": None,
"cognitoAuthenticationProvider": None,
"sourceIp": "127.0.0.1",
"accountId": None
},
"extendedRequestId": None,
"path": "/something/{event}"
},
"queryStringParameters": None,
"headers": {
"Host": "0.0.0.0:33651",
"User-Agent": "python-requests/{}".format(requests.__version__),
"Accept-Encoding": "gzip, deflate",
"Accept": "*/*",
"Connection": "keep-alive"
},
"pathParameters": {"event": "event1"},
"stageVariables": None,
"path": "/something/event1",
"isBase64Encoded": False
}

if body:
response["body"] = body
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/local/start_api/test_start_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,20 @@ class TestServiceResponses(StartApiIntegBaseClass):
def setUp(self):
self.url = "http://127.0.0.1:{}".format(self.port)

def test_multiple_headers_response(self):
response = requests.get(self.url + "/multipleheaders")

self.assertEquals(response.status_code, 200)
self.assertEquals(response.headers.get("Content-Type"), "text/plain")
self.assertEquals(response.headers.get("MyCustomHeader"), 'Value1, Value2')

def test_multiple_headers_overrides_headers_response(self):
response = requests.get(self.url + "/multipleheadersoverridesheaders")

self.assertEquals(response.status_code, 200)
self.assertEquals(response.headers.get("Content-Type"), "text/plain")
self.assertEquals(response.headers.get("MyCustomHeader"), 'Value1, Value2, Custom')

def test_binary_response(self):
"""
Binary data is returned correctly
Expand Down
Loading