moto/tests/test_cloudformation/test_cloudformation_custom_resources.py

230 lines
8.2 KiB
Python

import boto3
import json
import requests
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)
assert len(outputs) == 1
assert outputs[0]["OutputKey"] == "infokey"
assert outputs[0]["OutputValue"] == "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
)
assert execution_failed is True
printed_events = [
line for line in logs if line.startswith("{'RequestType': 'Create'")
]
assert len(printed_events) == 1
original_event = json.loads(printed_events[0].replace("'", '"'))
assert original_event["RequestType"] == "Create"
assert "ServiceToken" in original_event # Should equal Lambda ARN
assert "ResponseURL" in original_event
assert "StackId" in original_event
assert "RequestId" in original_event # type UUID
assert original_event["LogicalResourceId"] == "CustomInfo"
assert original_event["ResourceType"] == "Custom::Info"
assert "ResourceProperties" in original_event
assert (
"ServiceToken" in original_event["ResourceProperties"]
) # Should equal Lambda ARN
assert original_event["ResourceProperties"]["MyProperty"] == "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]
assert stack["Outputs"] == []
assert stack["StackStatus"] == "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]
assert stack["StackStatus"] == "CREATE_COMPLETE"
assert stack["Outputs"] == [
{"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"]
assert err["Code"] == "ValidationError"
assert (
err["Message"]
== "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