Skip to content

Commit 3a27f7b

Browse files
authored
refactor(test): Pre-create MSK/MQ in companion stack for testing (aws#3909)
1 parent 5d0a1be commit 3a27f7b

19 files changed

Lines changed: 203 additions & 466 deletions

integration/combination/test_function_with_mq.py

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from integration.config.service_names import MQ
77
from integration.helpers.base_test import BaseTest, nonblocking
8-
from integration.helpers.resource import current_region_does_not_support, generate_suffix
8+
from integration.helpers.resource import current_region_does_not_support
99

1010

1111
@skipIf(current_region_does_not_support([MQ]), "MQ is not supported in this testing region")
@@ -16,32 +16,23 @@ def companion_stack_outputs(self, get_companion_stack_outputs):
1616

1717
@parameterized.expand(
1818
[
19-
("combination/function_with_mq", "MQBrokerName", "MQBrokerUserSecretName", "PreCreatedSubnetOne"),
20-
(
21-
"combination/function_with_mq_using_autogen_role",
22-
"MQBrokerName2",
23-
"MQBrokerUserSecretName2",
24-
"PreCreatedSubnetTwo",
25-
),
19+
("combination/function_with_mq",),
20+
("combination/function_with_mq_using_autogen_role",),
2621
]
2722
)
2823
@nonblocking
29-
def test_function_with_mq(self, file_name, mq_broker, mq_secret, subnet_key):
24+
def test_function_with_mq(self, file_name):
3025
companion_stack_outputs = self.companion_stack_outputs
31-
parameters = self.get_parameters(companion_stack_outputs, subnet_key)
32-
secret_name = mq_secret + "-" + generate_suffix()
33-
parameters.append(self.generate_parameter(mq_secret, secret_name))
34-
secret_name = mq_broker + "-" + generate_suffix()
35-
parameters.append(self.generate_parameter(mq_broker, secret_name))
26+
parameters = [
27+
self.generate_parameter("PreCreatedMqBrokerArn", companion_stack_outputs["PreCreatedMqBrokerArn"]),
28+
self.generate_parameter(
29+
"PreCreatedMqBrokerSecretArn", companion_stack_outputs["PreCreatedMqBrokerSecretArn"]
30+
),
31+
]
3632

3733
self.create_and_verify_stack(file_name, parameters)
3834

39-
mq_client = self.client_provider.mq_client
40-
mq_broker_id = self.get_physical_id_by_type("AWS::AmazonMQ::Broker")
41-
broker_summary = get_broker_summary(mq_broker_id, mq_client)
42-
43-
self.assertEqual(len(broker_summary), 1, "One MQ cluster should be present")
44-
mq_broker_arn = broker_summary[0]["BrokerArn"]
35+
mq_broker_arn = companion_stack_outputs["PreCreatedMqBrokerArn"]
4536

4637
lambda_client = self.client_provider.lambda_client
4738
function_name = self.get_physical_id_by_type("AWS::Lambda::Function")
@@ -54,15 +45,3 @@ def test_function_with_mq(self, file_name, mq_broker, mq_secret, subnet_key):
5445

5546
self.assertEqual(event_source_mapping_function_arn, lambda_function_arn)
5647
self.assertEqual(event_source_mapping_mq_broker_arn, mq_broker_arn)
57-
58-
def get_parameters(self, dictionary, subnet_key):
59-
parameters = []
60-
parameters.append(self.generate_parameter("PreCreatedVpc", dictionary["PreCreatedVpc"]))
61-
parameters.append(self.generate_parameter(subnet_key, dictionary[subnet_key]))
62-
parameters.append(self.generate_parameter("PreCreatedInternetGateway", dictionary["PreCreatedInternetGateway"]))
63-
return parameters
64-
65-
66-
def get_broker_summary(mq_broker_id, mq_client):
67-
broker_summaries = mq_client.list_brokers()["BrokerSummaries"]
68-
return [broker_summary for broker_summary in broker_summaries if broker_summary["BrokerId"] == mq_broker_id]

integration/combination/test_function_with_msk.py

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@
44

55
from integration.config.service_names import MSK
66
from integration.helpers.base_test import BaseTest, nonblocking
7-
from integration.helpers.resource import current_region_does_not_support, generate_suffix
7+
from integration.helpers.resource import current_region_does_not_support
88

99

10-
# Mark this test suite as nonblocking tests since MSK Cluster creation can take
11-
# up to 30 minutes according to https://docs.aws.amazon.com/msk/latest/developerguide/troubleshooting.html#troubleshooting-cluster-stuck
12-
# This would cause the test to fail due to MSK Cluster did not stablize.
13-
# We should investigate any other cause of failures.
1410
@skipIf(current_region_does_not_support([MSK]), "MSK is not supported in this testing region")
1511
@nonblocking
1612
class TestFunctionWithMsk(BaseTest):
@@ -19,38 +15,25 @@ def companion_stack_outputs(self, get_companion_stack_outputs):
1915
self.companion_stack_outputs = get_companion_stack_outputs
2016

2117
def test_function_with_msk_trigger(self):
22-
companion_stack_outputs = self.companion_stack_outputs
23-
parameters = self.get_parameters(companion_stack_outputs)
24-
cluster_name = "MskCluster-" + generate_suffix()
25-
parameters.append(self.generate_parameter("MskClusterName", cluster_name))
18+
parameters = self.get_parameters()
2619
self._common_validations_for_MSK("combination/function_with_msk", parameters)
2720

2821
def test_function_with_msk_trigger_using_manage_policy(self):
29-
companion_stack_outputs = self.companion_stack_outputs
30-
parameters = self.get_parameters(companion_stack_outputs)
31-
cluster_name = "MskCluster2-" + generate_suffix()
32-
parameters.append(self.generate_parameter("MskClusterName2", cluster_name))
22+
parameters = self.get_parameters()
3323
self._common_validations_for_MSK("combination/function_with_msk_using_managed_policy", parameters)
3424

3525
def test_function_with_msk_trigger_and_s3_onfailure_events_destinations(self):
36-
companion_stack_outputs = self.companion_stack_outputs
37-
parameters = self.get_parameters(companion_stack_outputs)
38-
cluster_name = "MskCluster3-" + generate_suffix()
39-
parameters.append(self.generate_parameter("MskClusterName3", cluster_name))
26+
parameters = self.get_parameters()
4027
self._common_validations_for_MSK(
4128
"combination/function_with_msk_trigger_and_s3_onfailure_events_destinations", parameters
4229
)
4330

4431
def test_function_with_msk_trigger_and_premium_features(self):
45-
companion_stack_outputs = self.companion_stack_outputs
46-
parameters = self.get_parameters(companion_stack_outputs)
47-
cluster_name = "MskCluster4-" + generate_suffix()
48-
parameters.append(self.generate_parameter("MskClusterName4", cluster_name))
32+
parameters = self.get_parameters()
4933
self._common_validations_for_MSK("combination/function_with_msk_trigger_and_premium_features", parameters)
5034
event_source_mapping_result = self._common_validations_for_MSK(
5135
"combination/function_with_msk_trigger_and_confluent_schema_registry", parameters
5236
)
53-
# Verify error handling properties are correctly set
5437
self.assertTrue(event_source_mapping_result.get("BisectBatchOnFunctionError"))
5538
self.assertEqual(event_source_mapping_result.get("MaximumRecordAgeInSeconds"), 3600)
5639
self.assertEqual(event_source_mapping_result.get("MaximumRetryAttempts"), 3)
@@ -59,15 +42,8 @@ def test_function_with_msk_trigger_and_premium_features(self):
5942
def _common_validations_for_MSK(self, file_name, parameters):
6043
self.create_and_verify_stack(file_name, parameters)
6144

62-
kafka_client = self.client_provider.kafka_client
45+
msk_cluster_arn = self.companion_stack_outputs["PreCreatedMskClusterArn"]
6346

64-
msk_cluster_id = self.get_physical_id_by_type("AWS::MSK::Cluster")
65-
cluster_info_list = kafka_client.list_clusters()["ClusterInfoList"]
66-
cluster_info = [x for x in cluster_info_list if x["ClusterArn"] == msk_cluster_id]
67-
68-
self.assertEqual(len(cluster_info), 1, "One MSK cluster should be present")
69-
70-
msk_cluster_arn = cluster_info[0]["ClusterArn"]
7147
lambda_client = self.client_provider.lambda_client
7248
function_name = self.get_physical_id_by_type("AWS::Lambda::Function")
7349
lambda_function_arn = lambda_client.get_function_configuration(FunctionName=function_name)["FunctionArn"]
@@ -82,8 +58,7 @@ def _common_validations_for_MSK(self, file_name, parameters):
8258
self.assertEqual(event_source_mapping_kafka_cluster_arn, msk_cluster_arn)
8359
return event_source_mapping_result
8460

85-
def get_parameters(self, dictionary):
86-
parameters = []
87-
parameters.append(self.generate_parameter("PreCreatedSubnetOne", dictionary["PreCreatedSubnetOne"]))
88-
parameters.append(self.generate_parameter("PreCreatedSubnetTwo", dictionary["PreCreatedSubnetTwo"]))
89-
return parameters
61+
def get_parameters(self):
62+
return [
63+
self.generate_parameter("PreCreatedMskClusterArn", self.companion_stack_outputs["PreCreatedMskClusterArn"]),
64+
]

integration/conftest.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
2+
import os
23
from pathlib import Path
34

45
import boto3
56
import botocore
67
import pytest
78
from botocore.exceptions import ClientError
89

10+
from integration.config.service_names import MQ, MSK
911
from integration.helpers.base_test import S3_BUCKET_PREFIX
1012
from integration.helpers.client_provider import ClientProvider
1113
from integration.helpers.deployer.exceptions.exceptions import S3DoesNotExistException, ThrottlingError
@@ -20,7 +22,7 @@
2022

2123
LOG = logging.getLogger(__name__)
2224

23-
COMPANION_STACK_NAME = "sam-integ-stack-companion"
25+
COMPANION_STACK_NAME = os.environ.get("COMPANION_STACK_NAME", "sam-integ-stack-companion")
2426
COMPANION_STACK_TEMPLATE = "companion-stack.yaml"
2527
SAR_APP_TEMPLATE = "example-sar-app.yaml"
2628
SAR_APP_NAME = "sam-integration-test-sar-app"
@@ -62,14 +64,29 @@ def clean_all_integ_buckets():
6264

6365
@pytest.fixture()
6466
def setup_companion_stack_once(tmpdir_factory, get_prefix):
67+
# When COMPANION_STACK_NAME is set via env var, the stack is pre-created by the test platform
68+
if os.environ.get("COMPANION_STACK_NAME"):
69+
return
6570
tests_integ_dir = Path(__file__).resolve().parents[1]
6671
template_folder = Path(tests_integ_dir, "integration", "setup")
6772
companion_stack_template_path = Path(template_folder, COMPANION_STACK_TEMPLATE)
6873
cfn_client = ClientProvider().cfn_client
6974
output_dir = tmpdir_factory.mktemp("data")
7075
stack_name = get_prefix + COMPANION_STACK_NAME
7176
companion_stack = Stack(stack_name, companion_stack_template_path, cfn_client, output_dir)
72-
companion_stack.create_or_update(_stack_exists(stack_name))
77+
parameters = _companion_stack_parameters()
78+
companion_stack.create_or_update(_stack_exists(stack_name), parameters)
79+
80+
81+
def _companion_stack_parameters():
82+
"""Return companion stack parameters, disabling MSK/MQ in unsupported regions."""
83+
84+
params = []
85+
if current_region_does_not_support([MSK]):
86+
params.append({"ParameterKey": "CreateMskCluster", "ParameterValue": "false"})
87+
if current_region_does_not_support([MQ]):
88+
params.append({"ParameterKey": "CreateMqBroker", "ParameterValue": "false"})
89+
return params
7390

7491

7592
@pytest.fixture()
@@ -155,6 +172,8 @@ def get_s3_uri(file_name, uri_type, bucket, region):
155172

156173
@pytest.fixture()
157174
def delete_companion_stack_once(get_prefix):
175+
if os.environ.get("COMPANION_STACK_NAME"):
176+
return
158177
if not get_prefix:
159178
ClientProvider().cfn_client.delete_stack(StackName=COMPANION_STACK_NAME)
160179

@@ -179,7 +198,11 @@ def get_stack_outputs(stack_description):
179198

180199
@pytest.fixture()
181200
def get_companion_stack_outputs(get_prefix):
182-
companion_stack_description = get_stack_description(get_prefix + COMPANION_STACK_NAME)
201+
if os.environ.get("COMPANION_STACK_NAME"):
202+
stack_name = COMPANION_STACK_NAME
203+
else:
204+
stack_name = get_prefix + COMPANION_STACK_NAME
205+
companion_stack_description = get_stack_description(stack_name)
183206
return get_stack_outputs(companion_stack_description)
184207

185208

integration/helpers/base_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def setUpClass(cls):
9393
cls.tests_integ_dir = Path(__file__).resolve().parents[1]
9494
cls.resources_dir = Path(cls.tests_integ_dir, "resources")
9595
cls.template_dir = Path(cls.resources_dir, "templates")
96-
cls.output_dir = Path(cls.tests_integ_dir, "tmp" + "-" + generate_suffix())
96+
cls.output_dir = Path("/tmp", "tmp-" + generate_suffix())
9797
cls.expected_dir = Path(cls.resources_dir, "expected")
9898
cls.code_dir = Path(cls.resources_dir, "code")
9999
cls.session = boto3.session.Session()

integration/helpers/resource.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,21 +145,26 @@ def _get_region():
145145
return region
146146

147147

148+
_TMP_CONFIG_DIR = Path("/tmp/integration/config")
149+
150+
148151
def read_test_config_file(filename):
149152
"""Reads test config file and returns the contents"""
150153
tests_integ_dir = Path(__file__).resolve().parents[1]
151154
test_config_file_path = Path(tests_integ_dir, "config", filename)
155+
tmp_path = Path(_TMP_CONFIG_DIR, filename)
156+
if not test_config_file_path.is_file() and tmp_path.is_file():
157+
test_config_file_path = tmp_path
152158
if not test_config_file_path.is_file():
153159
return {}
154160
test_config = load_yaml(str(test_config_file_path))
155161
return test_config
156162

157163

158164
def write_test_config_file_to_json(filename, input):
159-
"""Reads test config file and returns the contents"""
160-
tests_integ_dir = Path(__file__).resolve().parents[1]
161-
test_config_file_path = Path(tests_integ_dir, "config", filename)
162-
with open(test_config_file_path, "w") as f:
165+
"""Writes test config file as JSON to /tmp for portability across environments."""
166+
_TMP_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
167+
with open(Path(_TMP_CONFIG_DIR, filename), "w") as f:
163168
json.dump(input, f)
164169

165170

integration/helpers/stack.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ def __init__(self, stack_name, template_path, cfn_client, output_dir):
1919
self.stack_description = None
2020
self.stack_resources = None
2121

22-
def create_or_update(self, update):
22+
def create_or_update(self, update, parameters=None):
2323
output_template_path = self._generate_output_file_path(self.template_path, self.output_dir)
2424
transform_template(self.template_path, output_template_path)
25-
self._deploy_stack(output_template_path, update)
25+
self._deploy_stack(output_template_path, update, parameters)
2626

2727
def delete(self):
2828
self.cfn_client.delete_stack(StackName=self.stack_name)

integration/resources/expected/combination/function_with_mq.json

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,8 @@
77
"LogicalResourceId": "MyLambdaExecutionRole",
88
"ResourceType": "AWS::IAM::Role"
99
},
10-
{
11-
"LogicalResourceId": "PublicSubnetRouteTableAssociation",
12-
"ResourceType": "AWS::EC2::SubnetRouteTableAssociation"
13-
},
14-
{
15-
"LogicalResourceId": "MQSecurityGroup",
16-
"ResourceType": "AWS::EC2::SecurityGroup"
17-
},
18-
{
19-
"LogicalResourceId": "MyMqBroker",
20-
"ResourceType": "AWS::AmazonMQ::Broker"
21-
},
22-
{
23-
"LogicalResourceId": "RouteTable",
24-
"ResourceType": "AWS::EC2::RouteTable"
25-
},
26-
{
27-
"LogicalResourceId": "MQBrokerUserSecret",
28-
"ResourceType": "AWS::SecretsManager::Secret"
29-
},
3010
{
3111
"LogicalResourceId": "MyLambdaFunctionMyMqEvent",
3212
"ResourceType": "AWS::Lambda::EventSourceMapping"
33-
},
34-
{
35-
"LogicalResourceId": "Route",
36-
"ResourceType": "AWS::EC2::Route"
3713
}
3814
]

integration/resources/expected/combination/function_with_mq_using_autogen_role.json

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,8 @@
77
"LogicalResourceId": "MyLambdaFunctionRole",
88
"ResourceType": "AWS::IAM::Role"
99
},
10-
{
11-
"LogicalResourceId": "PublicSubnetRouteTableAssociation",
12-
"ResourceType": "AWS::EC2::SubnetRouteTableAssociation"
13-
},
14-
{
15-
"LogicalResourceId": "MQSecurityGroup",
16-
"ResourceType": "AWS::EC2::SecurityGroup"
17-
},
18-
{
19-
"LogicalResourceId": "MyMqBroker",
20-
"ResourceType": "AWS::AmazonMQ::Broker"
21-
},
22-
{
23-
"LogicalResourceId": "RouteTable",
24-
"ResourceType": "AWS::EC2::RouteTable"
25-
},
26-
{
27-
"LogicalResourceId": "MQBrokerUserSecret",
28-
"ResourceType": "AWS::SecretsManager::Secret"
29-
},
3010
{
3111
"LogicalResourceId": "MyLambdaFunctionMyMqEvent",
3212
"ResourceType": "AWS::Lambda::EventSourceMapping"
33-
},
34-
{
35-
"LogicalResourceId": "Route",
36-
"ResourceType": "AWS::EC2::Route"
3713
}
3814
]

integration/resources/expected/combination/function_with_msk.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
"LogicalResourceId": "MyLambdaExecutionRole",
88
"ResourceType": "AWS::IAM::Role"
99
},
10-
{
11-
"LogicalResourceId": "MyMskCluster",
12-
"ResourceType": "AWS::MSK::Cluster"
13-
},
1410
{
1511
"LogicalResourceId": "MyMskStreamProcessorMyMskEvent",
1612
"ResourceType": "AWS::Lambda::EventSourceMapping"

integration/resources/expected/combination/function_with_msk_trigger_and_premium_features.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
"LogicalResourceId": "MyLambdaExecutionRole",
88
"ResourceType": "AWS::IAM::Role"
99
},
10-
{
11-
"LogicalResourceId": "MyMskCluster",
12-
"ResourceType": "AWS::MSK::Cluster"
13-
},
1410
{
1511
"LogicalResourceId": "MyMskStreamProcessorMyMskEvent",
1612
"ResourceType": "AWS::Lambda::EventSourceMapping"

0 commit comments

Comments
 (0)