import boto3 import json import requests import sure # noqa # pylint: disable=unused-import import time import pytest from botocore.exceptions import ClientError from moto import mock_lambda, mock_cloudformation, mock_logs, mock_s3, settings from unittest import SkipTest from uuid import uuid4 from tests.test_awslambda.utilities import wait_for_log_msg from .fixtures.custom_lambda import get_template, get_template_for_unknown_lambda from ..markers import requires_docker def get_lambda_code(): return f""" def lambda_handler(event, context): # Need to print this, one of the tests verifies the correct input print(event) response = dict() response["Status"] = "SUCCESS" response["StackId"] = event["StackId"] response["RequestId"] = event["RequestId"] response["LogicalResourceId"] = event["LogicalResourceId"] response["PhysicalResourceId"] = "CustomResource{str(uuid4())[0:6]}" response_data = dict() response_data["info_value"] = "special value" if event["RequestType"] == "Create": response["Data"] = response_data import cfnresponse cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) """ @mock_cloudformation @mock_lambda @mock_logs @mock_s3 def test_create_custom_lambda_resource(): ######### # Integration test using a Custom Resource # Create a Lambda # CF will call the Lambda # The Lambda should call CF, to indicate success (using the cfnresponse-module) # This HTTP request will include any outputs that are now stored against the stack # TEST: verify that this output is persisted ########## if not settings.TEST_SERVER_MODE: raise SkipTest( "Needs a standalone MotoServer, as cfnresponse needs to connect to something" ) # Create cloudformation stack stack_name = f"stack{str(uuid4())[0:6]}" template_body = get_template(get_lambda_code()) cf = boto3.client("cloudformation", region_name="us-east-1") cf.create_stack( StackName=stack_name, TemplateBody=json.dumps(template_body), Capabilities=["CAPABILITY_IAM"], ) # Verify CloudWatch contains the correct logs log_group_name = get_log_group_name(cf, stack_name) success, logs = wait_for_log_msg( expected_msg="Status code: 200", log_group=log_group_name ) assert success, f"Logs should indicate success: \n{logs}" # Verify the correct Output was returned outputs = get_outputs(cf, stack_name) outputs.should.have.length_of(1) outputs[0].should.have.key("OutputKey").equals("infokey") outputs[0].should.have.key("OutputValue").equals("special value") @mock_cloudformation @mock_lambda @mock_logs @mock_s3 @requires_docker def test_create_custom_lambda_resource__verify_cfnresponse_failed(): ######### # Integration test using a Custom Resource # Create a Lambda # CF will call the Lambda # The Lambda should call CF --- this will fail, as we cannot make a HTTP request to the in-memory moto decorators # TEST: verify that the original event was send to the Lambda correctly # TEST: verify that a failure message appears in the CloudwatchLogs ########## if settings.TEST_SERVER_MODE: raise SkipTest("Verify this fails if MotoServer is not running") # Create cloudformation stack stack_name = f"stack{str(uuid4())[0:6]}" template_body = get_template(get_lambda_code()) cf = boto3.client("cloudformation", region_name="us-east-1") cf.create_stack( StackName=stack_name, TemplateBody=json.dumps(template_body), Capabilities=["CAPABILITY_IAM"], ) # Verify CloudWatch contains the correct logs log_group_name = get_log_group_name(cf, stack_name) execution_failed, logs = wait_for_log_msg( expected_msg="failed executing http.request", log_group=log_group_name ) execution_failed.should.equal(True) printed_events = [ line for line in logs if line.startswith("{'RequestType': 'Create'") ] printed_events.should.have.length_of(1) original_event = json.loads(printed_events[0].replace("'", '"')) original_event.should.have.key("RequestType").equals("Create") original_event.should.have.key("ServiceToken") # Should equal Lambda ARN original_event.should.have.key("ResponseURL") original_event.should.have.key("StackId") original_event.should.have.key("RequestId") # type UUID original_event.should.have.key("LogicalResourceId").equals("CustomInfo") original_event.should.have.key("ResourceType").equals("Custom::Info") original_event.should.have.key("ResourceProperties") original_event["ResourceProperties"].should.have.key( "ServiceToken" ) # Should equal Lambda ARN original_event["ResourceProperties"].should.have.key("MyProperty").equals("stuff") @mock_cloudformation @mock_lambda @mock_logs @mock_s3 def test_create_custom_lambda_resource__verify_manual_request(): ######### # Integration test using a Custom Resource # Create a Lambda # CF will call the Lambda # The Lambda should call CF --- this will fail, as we cannot make a HTTP request to the in-memory moto decorators # So we'll make this HTTP request manually # TEST: verify that the stack has a CREATE_IN_PROGRESS status before making the HTTP request # TEST: verify that the stack has a CREATE_COMPLETE status afterwards ########## if settings.TEST_SERVER_MODE: raise SkipTest( "Verify HTTP request can be made manually if MotoServer is not running" ) # Create cloudformation stack stack_name = f"stack{str(uuid4())[0:6]}" template_body = get_template(get_lambda_code()) region_name = "eu-north-1" cf = boto3.client("cloudformation", region_name=region_name) stack = cf.create_stack( StackName=stack_name, TemplateBody=json.dumps(template_body), Capabilities=["CAPABILITY_IAM"], ) stack_id = stack["StackId"] stack = cf.describe_stacks(StackName=stack_id)["Stacks"][0] stack["Outputs"].should.equal([]) stack["StackStatus"].should.equal("CREATE_IN_PROGRESS") callback_url = f"http://cloudformation.{region_name}.amazonaws.com/cloudformation_{region_name}/cfnresponse?stack={stack_id}" data = { "Status": "SUCCESS", "StackId": stack_id, "LogicalResourceId": "CustomInfo", "Data": {"info_value": "resultfromthirdpartysystem"}, } requests.post(callback_url, json=data) stack = cf.describe_stacks(StackName=stack_id)["Stacks"][0] stack["StackStatus"].should.equal("CREATE_COMPLETE") stack["Outputs"].should.equal( [{"OutputKey": "infokey", "OutputValue": "resultfromthirdpartysystem"}] ) @mock_cloudformation def test_create_custom_lambda_resource__unknown_arn(): # Try to create a Lambda with an unknown ARN # Verify that this fails in a predictable manner cf = boto3.client("cloudformation", region_name="eu-north-1") with pytest.raises(ClientError) as exc: cf.create_stack( StackName=f"stack{str(uuid4())[0:6]}", TemplateBody=json.dumps(get_template_for_unknown_lambda()), Capabilities=["CAPABILITY_IAM"], ) err = exc.value.response["Error"] err["Code"].should.equal("ValidationError") err["Message"].should.equal( "Template error: instance of Fn::GetAtt references undefined resource InfoFunction" ) def get_log_group_name(cf, stack_name): resources = cf.describe_stack_resources(StackName=stack_name)["StackResources"] start = time.time() while (time.time() - start) < 5: fns = [ r for r in resources if r["ResourceType"] == "AWS::Lambda::Function" and "PhysicalResourceId" in r ] if not fns: time.sleep(1) resources = cf.describe_stack_resources(StackName=stack_name)[ "StackResources" ] continue fn = fns[0] resource_id = fn["PhysicalResourceId"] return f"/aws/lambda/{resource_id}" raise Exception("Could not find log group name in time") def get_outputs(cf, stack_name): stack = cf.describe_stacks(StackName=stack_name)["Stacks"][0] start = time.time() while (time.time() - start) < 5: status = stack["StackStatus"] if status != "CREATE_COMPLETE": time.sleep(1) stack = cf.describe_stacks(StackName=stack_name)["Stacks"][0] continue outputs = stack["Outputs"] return outputs