Skip to content

Commit

Permalink
Merge pull request #1208 from awslabs/develop
Browse files Browse the repository at this point in the history
chore: v0.17.0
  • Loading branch information
awood45 authored Jun 4, 2019
2 parents a681eb4 + f3cd892 commit dc81449
Show file tree
Hide file tree
Showing 27 changed files with 301 additions and 100 deletions.
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

0 comments on commit dc81449

Please sign in to comment.