Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .cfnlintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ ignore_templates:
- tests/translator/output/**/connector_sfn_to_function.json
- tests/translator/output/**/connector_sns_to_function.json
- tests/translator/output/**/connector_table_to_function.json
- tests/translator/output/**/graphqlapi_api_cache.json # Fail by design: intentionally invalid CachingConfig enum values to test error path
- tests/translator/output/**/eventbridgerule_with_dlq.json
- tests/translator/output/**/function_event_conditions.json
- tests/translator/output/**/function_with_condition.json
Expand Down Expand Up @@ -112,6 +113,10 @@ ignore_templates:
- tests/translator/output/**/function_with_logging_config.json # cfnlint is not updated to recognize the LoggingConfig property
- tests/translator/output/aws-*/*capacity_provider*.json # Ignore Capacity Provider test format in non-aws partitions
- tests/translator/output/**/capacity_provider_managed_resource_tags.json # cfnlint not updated for CapacityProvider mode (Phase 2)
- tests/translator/output/**/webfunction_basic.json # New AWS::Lambda::WebFunction* types cfn-lint does not recognize; DependsOn is intentional (W3005)
- tests/translator/output/**/webfunction_with_all_properties.json # New AWS::Lambda::WebFunction* types cfn-lint does not recognize; DependsOn is intentional (W3005)
- tests/translator/output/**/webfunction_multiregion.json # New AWS::Lambda::WebFunction* types cfn-lint does not recognize; DependsOn is intentional (W3005)
- tests/translator/output/**/basic_function.json # W1030: fixture uses Ref for VpcConfig SubnetIds, which cannot match the subnet-id pattern

ignore_checks:
- E2531 # Deprecated runtime; not relevant for transform tests
Expand Down
12 changes: 11 additions & 1 deletion bin/run_cfn_lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ set -eux

VENV=.venv_cfn_lint

# cfn-lint requires Python >= 3.10, which this project also requires (see python_requires in setup.py).
# Fail loudly rather than skipping: a silent "exit 0" here would turn the lint gate into a no-op on a
# misconfigured interpreter and report success without checking a single template.
PYTHON_MINOR=$(python3 -c "import sys; print(sys.version_info.minor)")
if [ "$PYTHON_MINOR" -lt 10 ]; then
echo "ERROR: cfn-lint requires Python >= 3.10, but python3 is 3.${PYTHON_MINOR}. Use a supported interpreter." >&2
exit 1
fi

# Install to separate venv to avoid circular dependency; cfn-lint depends on samtranslator
# See https://github.com/aws/serverless-application-model/issues/1042
if [ ! -d "${VENV}" ]; then
python3 -m venv "${VENV}"
fi

"${VENV}/bin/python" -m pip install cfn-lint --upgrade --quiet
# pinning 1.55.0 for now because there are some issues with 1.56.0
"${VENV}/bin/python" -m pip install cfn-lint==1.55.0 --upgrade --quiet
# update cfn schema with retry logic (can fail due to network issues)
# --regions us-east-1 avoids a cfn-lint bug where updating all regions causes a
# multiprocessing pickle error. See https://github.com/aws-cloudformation/cfn-lint/issues/4379
Expand Down
33 changes: 33 additions & 0 deletions integration/combination/test_function_with_capacity_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ def test_function_with_capacity_provider_default_role(self):
self.verify_capacity_provider_vpc_config(simple_cp_config, "SimpleCapacityProvider")
self.verify_capacity_provider_permissions_config(simple_cp_config, "SimpleCapacityProvider")

# Verify Globals.CapacityProvider.ManagedResourceTags inheritance on SimpleCapacityProvider
simple_propagate_tags = simple_cp_config.get("PropagateTags")
self.assertIsNotNone(simple_propagate_tags, "SimpleCapacityProvider should inherit PropagateTags from Globals")
self.assertEqual(
simple_propagate_tags.get("Mode"),
"Explicit",
"SimpleCapacityProvider should have PropagateTags Mode: Explicit from Globals",
)
simple_explicit_tags = simple_propagate_tags.get("ExplicitTags", {})
self.assertIn("GlobalTag", simple_explicit_tags, "SimpleCapacityProvider should inherit GlobalTag from Globals")
self.assertEqual(
simple_explicit_tags["GlobalTag"],
"shared-value",
"GlobalTag should be 'shared-value' from Globals",
)

advanced_cp_config = self.get_lambda_capacity_provider_config("AdvancedCapacityProvider")
self.verify_capacity_provider_basic_config(advanced_cp_config, "AdvancedCapacityProvider")
self.verify_capacity_provider_vpc_config(advanced_cp_config, "AdvancedCapacityProvider")
Expand Down Expand Up @@ -180,6 +196,23 @@ def test_function_with_capacity_provider_default_role(self):
"AdvancedCapacityProvider should use the correct KMS key",
)

# Verify ManagedResourceTags -> PropagateTags configuration
propagate_tags = advanced_cp_config.get("PropagateTags")
self.assertIsNotNone(propagate_tags, "AdvancedCapacityProvider should have PropagateTags configuration")
self.assertEqual(
propagate_tags.get("Mode"), "Explicit", "AdvancedCapacityProvider should have PropagateTags Mode: Explicit"
)

explicit_tags = propagate_tags.get("ExplicitTags", {})
self.assertIn("Environment", explicit_tags, "AdvancedCapacityProvider should propagate Environment tag")
self.assertIn("Team", explicit_tags, "AdvancedCapacityProvider should propagate Team tag")
self.assertIn("GlobalTag", explicit_tags, "AdvancedCapacityProvider should override GlobalTag from Globals")
self.assertEqual(explicit_tags["Environment"], "Production", "Environment tag should be 'Production'")
self.assertEqual(explicit_tags["Team"], "Lambda-Tooling", "Team tag should be 'Lambda-Tooling'")
self.assertEqual(
explicit_tags["GlobalTag"], "overridden-value", "GlobalTag should be overridden by resource-level value"
)

telemetry_config = advanced_cp_config.get("TelemetryConfig")
self.assertIsNotNone(telemetry_config, "AdvancedCapacityProvider should have TelemetryConfig")
logging_config = telemetry_config.get("LoggingConfig")
Expand Down
4 changes: 3 additions & 1 deletion integration/config/code_key_to_file_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
"definitionuri": "swagger1.json",
"microvmcodeuri": "microvm_code.zip",
"mtlsuri": "MTLSCert.pem",
"templateurl": "template.yaml"
"templateurl": "template.yaml",
"webfunccodeuri": "webfunc_code.zip",
"webfunccodeuri2": "webfunc_code2.zip"
}
8 changes: 8 additions & 0 deletions integration/config/file_to_s3_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@
"template.yaml": {
"type": "http",
"uri": ""
},
"webfunc_code.zip": {
"type": "s3",
"uri": ""
},
"webfunc_code2.zip": {
"type": "s3",
"uri": ""
}
}
5 changes: 5 additions & 0 deletions integration/config/region_service_inclusion.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
regions:
us-east-1:
- CustomDomain
- WebFunction
us-west-2:
- WebFunction
eu-west-1:
- WebFunction
Metadata:
SamTransformTest: true
1 change: 1 addition & 0 deletions integration/config/service_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
LAMBDA_URL = "LambdaUrl"
LAMBDA_MANAGED_INSTANCES = "LambdaManagedInstances"
MICROVM_IMAGE = "MicrovmImage"
WEB_FUNCTION = "WebFunction"
LAMBDA_ENV_VARS = "LambdaEnvVars"
EVENT_INVOKE_CONFIG = "EventInvokeConfig"
API_KEY = "ApiKey"
Expand Down
69 changes: 59 additions & 10 deletions integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,32 @@ def _get_all_buckets():
return s3.buckets.all()


def clean_bucket(s3_bucket_name, s3_client):
def empty_versioned_bucket(s3_bucket_name, s3_client):
"""
Empties and deletes the bucket used for the tests
Deletes every object version and delete marker from a bucket. Deleting objects without a VersionId
only adds delete markers on a versioned bucket, so the versions must be removed explicitly, otherwise
the bucket cannot be deleted (and CloudFormation's DeletionPolicy: Delete would leave the stack in
DELETE_FAILED). Works for non-versioned buckets too, where each object has a "null" version.
"""
s3 = boto3.resource("s3")
bucket = s3.Bucket(s3_bucket_name)
object_summary_iterator = bucket.objects.all()

for object_summary in object_summary_iterator:
paginator = s3_client.get_paginator("list_object_versions")
for page in paginator.paginate(Bucket=s3_bucket_name):
to_delete = [
{"Key": item["Key"], "VersionId": item["VersionId"]}
for item in (page.get("Versions", []) + page.get("DeleteMarkers", []))
]
if not to_delete:
continue
try:
s3_client.delete_object(Key=object_summary.key, Bucket=s3_bucket_name)
s3_client.delete_objects(Bucket=s3_bucket_name, Delete={"Objects": to_delete})
except ClientError as e:
LOG.error("Unable to delete object %s from bucket %s", object_summary.key, s3_bucket_name, exc_info=e)
LOG.error("Unable to delete object versions from bucket %s", s3_bucket_name, exc_info=e)


def clean_bucket(s3_bucket_name, s3_client):
"""
Empties and deletes the bucket used for the tests
"""
empty_versioned_bucket(s3_bucket_name, s3_client)
try:
s3_client.delete_bucket(Bucket=s3_bucket_name)
except ClientError as e:
Expand Down Expand Up @@ -89,6 +102,32 @@ def _companion_stack_parameters():
return params


@retry_with_exponential_backoff_and_jitter(
ThrottlingError,
5,
2,
exc_raise_msg="SAR ListApplications still throttled after 5 attempts",
)
def _sar_list_applications(sar_client):
"""
Wrap SAR ListApplications with the same throttle-retry treatment
the rest of this file applies to CloudFormation and S3 calls, so
a transient SAR throttle at fixture setup does not fail the whole test.

Unlike the CloudFormation and S3 helpers below, this uses a small base
delay: the fixture is function-scoped and autoused by TestBasicApplication,
so the backoff is paid per test. A read-only ListApplications throttle
clears in seconds, so ~1 minute of total backoff is enough.
"""
try:
return sar_client.list_applications().get("Applications", [])
except botocore.exceptions.ClientError as ex:
error_code = ex.response.get("Error", {}).get("Code", "")
if error_code in ("TooManyRequestsException", "Throttling", "ThrottlingException"):
raise ThrottlingError(stack_name="SAR:list_applications", msg=str(ex))
raise


@pytest.fixture()
def get_serverless_application_repository_app():
"""Create or re-use a simple SAR app"""
Expand All @@ -97,7 +136,7 @@ def get_serverless_application_repository_app():
return None

sar_client = ClientProvider().sar_client
sar_apps = sar_client.list_applications().get("Applications", [])
sar_apps = _sar_list_applications(sar_client)
for sar_app in sar_apps:
if sar_app.get("Name") == SAR_APP_NAME:
LOG.info("SAR Application was already created, skipping SAR application publish")
Expand Down Expand Up @@ -175,6 +214,16 @@ def delete_companion_stack_once(get_prefix):
if os.environ.get("COMPANION_STACK_NAME"):
return
if not get_prefix:
# PreCreatedS3Bucket has versioning enabled, so CloudFormation cannot delete it while it still holds
# object versions or delete markers — the stack would be left in DELETE_FAILED and leak the bucket.
# Empty all versions first so DeletionPolicy: Delete can succeed.
try:
outputs = get_stack_outputs(get_stack_description(COMPANION_STACK_NAME))
bucket_name = outputs.get("PreCreatedS3Bucket")
if bucket_name:
empty_versioned_bucket(bucket_name, ClientProvider().s3_client)
except botocore.exceptions.ClientError as e:
LOG.error("Unable to empty companion bucket before deleting the companion stack", exc_info=e)
ClientProvider().cfn_client.delete_stack(StackName=COMPANION_STACK_NAME)


Expand Down
Binary file added integration/resources/code/webfunc_code.zip
Binary file not shown.
Binary file added integration/resources/code/webfunc_code2.zip
Binary file not shown.
18 changes: 18 additions & 0 deletions integration/resources/expected/single/basic_web_function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "CustomRole",
"ResourceType": "AWS::IAM::Role"
},
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"LogicalResourceId": "MyWebFunction",
"ResourceType": "AWS::Lambda::WebFunction"
},
{
"LogicalResourceId": "MyWebFunctionEndpoint",
"ResourceType": "AWS::Lambda::WebFunctionEndpoint"
},
{
"LogicalResourceId": "MyWebFunctionRevision",
"ResourceType": "AWS::Lambda::WebFunctionRevision"
},
{
"LogicalResourceId": "MyWebFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Loading
Loading