Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow cfn_response to be called inside handler #4

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
27 changes: 4 additions & 23 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ your AWS Lambda function.

from cfnlambda import handler_decorator

@handler_decorator()
@handler_decorator
def lambda_handler(event, context):
sum = (float(event['ResourceProperties']['key1']) +
float(event['ResourceProperties']['key2']))
Expand Down Expand Up @@ -104,7 +104,7 @@ First, this Lambda code must be zipped and uploaded to an s3 bucket.
import logging
logging.getLogger().setLevel(logging.INFO)

@handler_decorator()
@handler_decorator
def lambda_handler(event, context):
sum = (float(event['ResourceProperties']['key1']) +
float(event['ResourceProperties']['key2']))
Expand All @@ -123,7 +123,7 @@ Here are a set of commands to create and upload the AWS Lambda function
import logging
logging.getLogger().setLevel(logging.INFO)

@handler_decorator()
@handler_decorator
def lambda_handler(event, context):
sum = (float(event['ResourceProperties']['key1']) +
float(event['ResourceProperties']['key2']))
Expand Down Expand Up @@ -299,23 +299,4 @@ key of the author of `cfnlambda`. Go to `keybase`_ and type the `key ID` into
the search bar. You should get back a single user's profile which lists out a
collection of accounts that the user has proved control of. A strong indicator
that the person is the author is if you can find `cfnlambda` in their github
account.

FAQ
---

Q: What causes the error `inner_decorator() takes exactly 1 argument (2 given): TypeError Traceback
(most recent call last): File "/var/runtime/awslambda/bootstrap.py", line
177, in handle_event_request result = request_handler(json_input, context)
TypeError: inner_decorator() takes exactly 1 argument (2 given)`

A: You likely used `@handler_decorator` to decorate your function instead of
`@handler_decorator()`. Because `handler_decorator` accepts arguments, you need
to use it with parenthesis.

.. _AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/s3/index.html
.. _install and configure AWS CLI: http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-set-up.html
.. _returning: https://docs.python.org/2/reference/simple_stmts.html#return
.. _cfn-response: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule
.. _downloads section on PyPI: https://pypi.python.org/pypi/cfnlambda#downloads
.. _keybase: https://keybase.io/
account.
148 changes: 119 additions & 29 deletions cfnlambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from functools import wraps
import boto3
from botocore.vendored import requests
import traceback
import httplib

logger = logging.getLogger(__name__)

Expand All @@ -28,9 +30,55 @@ class Status:

http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html
"""
SUCCESS = 'SUCCESS'
FAILED = 'FAILED'

def __init__(self, value, reason=None, put_response=None):
self.value = value
self.reason = reason
self.put_response = put_response

def __repr__(self):
r = '{}'.format(self.value)
if self.reason:
r += '({})'.format(self.reason)
return r

def isSuccess(self):
return self.value == 'SUCCESS' or (self.isFinished() and self.value.value == 'SUCCESS')

def isFailed(self):
return self.value == 'FAILED' or (self.isFinished() and self.value.value == 'FAILED')

def isFinished(self):
return isinstance(self.value, Status)

@classmethod
def getFailed(cls, reason):
return cls('FAILED', reason)

@classmethod
def getFinished(cls, status, put_response=None):
return cls(status, put_response=put_response)

Status.SUCCESS = Status('SUCCESS')
Status.FAILED = Status('FAILED')

class Result(object):
def __init__(self, physical_resource_id, data=None):
self.physical_resource_id = physical_resource_id
self.data = data
self.dict = {}
self.status = None

def success(self):
self.status = Status.SUCCESS
return self

def failed(self, reason):
self.status = Status.getFailed(reason)
return self

def finish(self, event, context):
return Status.getFinished(self.status, put_response=cfn_response(event, context, self.status, physical_resource_id=self.physical_resource_id, response_data=self.data))

class RequestType:
"""CloudFormation custom resource request type constants
Expand Down Expand Up @@ -83,7 +131,9 @@ def cfn_response(event,
information.[4]
response_status: A status of SUCCESS or FAILED to send back to
CloudFormation.[2] Use the Status.SUCCESS and Status.FAILED
constants.
constants, or Status.getFailed() to provide a reason for the
failure. If the status was wrapped using Status.getFinished(),
the call is a noop and returns None.
response_data: A dictionary of key value pairs to pass back to
CloudFormation which can be accessed with the Fn::GetAtt function
on the CloudFormation custom resource.[5]
Expand All @@ -92,7 +142,7 @@ def cfn_response(event,
CloudWatch Logs log stream is used.

Returns:
None
requests.Response object

Raises:
No exceptions raised
Expand All @@ -103,12 +153,16 @@ def cfn_response(event,
[4]: http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
[5]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html#crpg-ref-responses-data
"""
if isinstance(response_status, Status) and response_status.isFinished():
return

if physical_resource_id is None:
physical_resource_id = context.log_stream_name
default_reason = ("See the details in CloudWatch Log Stream: %s" %
context.log_stream_name)
body = {
"Status": response_status,
"Reason": ("See the details in CloudWatch Log Stream: %s" %
context.log_stream_name),
"Status": response_status.value if isinstance(response_status, Status) else response_status,
"Reason": response_status.reason or default_reason if isinstance(response_status, Status) else default_reason,
"PhysicalResourceId": physical_resource_id,
"StackId": event['StackId'],
"RequestId": event['RequestId'],
Expand All @@ -120,21 +174,32 @@ def cfn_response(event,
try:
response = requests.put(event['ResponseURL'],
data=response_body)
logger.debug("Status code: %s" % response.status_code)
body_text = ""
if response.status_code // 100 != 2:
body_text = "\n" + response.text
logger.debug("Status code: %s %s%s" % (response.status_code, httplib.responses[response.status_code], body_text))

# logger.debug("Status message: %s" % response.status_message)
# how do we get the status message?
return response
except Exception as e:
logger.error("send(..) failed executing https.request(..): %s" %
e.message)
logger.debug(traceback.format_exc())


def handler_decorator(delete_logs=True,
hide_stack_delete_failure=True):
def handler_decorator(*args, **kwargs):
"""Decorate an AWS Lambda function to add exception handling, emit
CloudFormation responses and log.

Usage:
>>> @handler_decorator()
>>> @handler_decorator
... def lambda_handler(event, context):
... sum = (float(event['ResourceProperties']['key1']) +
... float(event['ResourceProperties']['key2']))
... return {'sum': sum}

>>> @handler_decorator(delete_logs=False)
... def lambda_handler(event, context):
... sum = (float(event['ResourceProperties']['key1']) +
... float(event['ResourceProperties']['key2']))
Expand All @@ -161,6 +226,11 @@ def handler_decorator(delete_logs=True,
Raises:
No exceptions
"""
if args:
return handler_decorator()(args[0])

delete_logs = kwargs.get('delete_logs', True)
hide_stack_delete_failure = kwargs.get('hide_stack_delete_failure', True)

def inner_decorator(handler):
"""Bind handler_decorator to handler_wrapper in order to enable passing
Expand Down Expand Up @@ -194,7 +264,13 @@ def handler_wrapper(event, context):
information.[2]

Returns:
None
If the handler returns a Status object, the wrapper returns an
empty dict.

If the handler returns two values, the first being a Status object,
the wrapper returns the second value.

Otherwise, the wrapper returns the value returned by the handler.

Returns to CloudFormation:
TODO
Expand All @@ -208,19 +284,30 @@ def handler_wrapper(event, context):
logger.info('REQUEST RECEIVED: %s' % json.dumps(event))
logger.info('LambdaContext: %s' %
json.dumps(vars(context), cls=PythonObjectEncoder))
result = None
try:
result = handler(event, context)
status = Status.SUCCESS if result else Status.FAILED
if not status:
if isinstance(result, Status):
status = result
result = None
elif isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], Status):
status, result = result
else:
status = Status.SUCCESS if result else Status.FAILED
if result is False:
message = "Function %s returned False." % handler.__name__
logger.error(message)
status = Status.FAILED
result = {'result': message}
except Exception as e:
status = Status.FAILED
message = ('Function %s failed due to exception "%s".' %
status = Status.getFailed('Function %s failed due to exception "%s".' %
(handler.__name__, e.message))
result = {'result': message}
logger.error(message)
result = {}
logger.error(status.reason)
logger.debug(traceback.format_exc())

if not result:
result = {}

if event['RequestType'] == RequestType.DELETE:
if status == Status.FAILED and hide_stack_delete_failure:
Expand All @@ -230,19 +317,22 @@ def handler_wrapper(event, context):
'despite the fact that the stack status may be '
'DELETE_COMPLETE.')
logger.error(message)
result['result'] += ' %s' % message
result['result'] = result.get('result', '') + ' %s' % message
status = Status.SUCCESS

if status == Status.SUCCESS and delete_logs:
if status.isSuccess() and delete_logs:
logging.disable(logging.CRITICAL)
logs_client = boto3.client('logs')
logs_client.delete_log_group(
logGroupName=context.log_group_name)
cfn_response(event,
context,
status,
(result if type(result) is dict else
{'result': result}))
return handler(event, context)
logs_client.delete_log_stream(
logGroupName=context.log_group_name,
logStreamName=context.log_stream_name)
result = (dict(result) if isinstance(result, dict) else {'result': result})
if not status.isFinished():
cfn_response(event,
context,
status,
result,
)
return result
return handler_wrapper
return inner_decorator
return inner_decorator