Skip to content

Commit fdb75ab

Browse files
committed
Complete SAM implicit resource injection: DeploymentPreference, event permissions, Api Domain/UsagePlan, implicit API stages
1 parent 63c0056 commit fdb75ab

1 file changed

Lines changed: 90 additions & 76 deletions

File tree

src/cfnlint/context/context.py

Lines changed: 90 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,17 @@ def _init_transforms(transforms: Any) -> Transforms:
568568
return Transforms([])
569569

570570

571+
def _inject(
572+
resources: dict[str, Resource], logical_id: str, resource_type: str
573+
) -> None:
574+
"""Add a synthetic resource if it doesn't already exist."""
575+
if logical_id not in resources:
576+
try:
577+
resources[logical_id] = Resource({"Type": resource_type})
578+
except ValueError:
579+
pass
580+
581+
571582
def _inject_sam_implicit_resources(
572583
template_resources: Any, resources: dict[str, Resource]
573584
) -> None:
@@ -596,92 +607,95 @@ def _inject_sam_implicit_resources(
596607
"AWS::Serverless::Function",
597608
"AWS::Serverless::StateMachine",
598609
):
599-
role_id = f"{resource_id}Role"
600-
if "Role" not in props and role_id not in resources:
601-
try:
602-
resources[role_id] = Resource({"Type": "AWS::IAM::Role"})
603-
except ValueError:
604-
pass
610+
if "Role" not in props:
611+
_inject(resources, f"{resource_id}Role", "AWS::IAM::Role")
605612

606613
if resource_type == "AWS::Serverless::Function":
607-
# SAM generates Version/Alias resources when AutoPublishAlias
608-
# or DeploymentPreference is set. SAM supports !Ref {Id}.Version
609-
# and !Ref {Id}.Alias as special syntax.
614+
# Version/Alias when AutoPublishAlias or DeploymentPreference
610615
has_alias = "AutoPublishAlias" in props or "DeploymentPreference" in props
611616
if has_alias:
612-
version_id = f"{resource_id}.Version"
613-
if version_id not in resources:
614-
try:
615-
resources[version_id] = Resource(
616-
{"Type": "AWS::Lambda::Version"}
617-
)
618-
except ValueError:
619-
pass
620-
alias_id = f"{resource_id}.Alias"
621-
if alias_id not in resources:
622-
try:
623-
resources[alias_id] = Resource({"Type": "AWS::Lambda::Alias"})
624-
except ValueError:
625-
pass
626-
627-
# SAM generates a Url resource when FunctionUrlConfig is set
617+
for suffix, rtype in (
618+
(f"{resource_id}.Version", "AWS::Lambda::Version"),
619+
(f"{resource_id}.Alias", "AWS::Lambda::Alias"),
620+
):
621+
if suffix not in resources:
622+
try:
623+
resources[suffix] = Resource({"Type": rtype})
624+
except ValueError:
625+
pass
626+
627+
# Url when FunctionUrlConfig is set
628628
if "FunctionUrlConfig" in props:
629-
url_id = f"{resource_id}Url"
630-
if url_id not in resources:
631-
try:
632-
resources[url_id] = Resource({"Type": "AWS::Lambda::Url"})
633-
except ValueError:
634-
pass
635-
636-
# SAM Api/HttpApi always generate Stage resources
629+
_inject(resources, f"{resource_id}Url", "AWS::Lambda::Url")
630+
631+
# DeploymentPreference generates CodeDeploy resources
632+
dp = props.get("DeploymentPreference", {})
633+
if isinstance(dp, dict) and dp.get("Enabled", True):
634+
_inject(
635+
resources,
636+
"ServerlessDeploymentApplication",
637+
"AWS::CodeDeploy::Application",
638+
)
639+
_inject(
640+
resources,
641+
f"{resource_id}DeploymentGroup",
642+
"AWS::CodeDeploy::DeploymentGroup",
643+
)
644+
if "Role" not in dp:
645+
_inject(resources, "CodeDeployServiceRole", "AWS::IAM::Role")
646+
647+
# Per-event permissions and implicit API detection
648+
events = props.get("Events", {})
649+
if isinstance(events, dict):
650+
for event_name, event in events.items():
651+
if not isinstance(event, dict):
652+
continue
653+
_inject(
654+
resources,
655+
f"{resource_id}{event_name}Permission",
656+
"AWS::Lambda::Permission",
657+
)
658+
event_type = event.get("Type")
659+
if event_type == "Api":
660+
event_props = event.get("Properties", {})
661+
if (
662+
not isinstance(event_props, dict)
663+
or "RestApiId" not in event_props
664+
):
665+
needs_rest_api = True
666+
elif event_type == "HttpApi":
667+
event_props = event.get("Properties", {})
668+
if (
669+
not isinstance(event_props, dict)
670+
or "ApiId" not in event_props
671+
):
672+
needs_http_api = True
673+
637674
if resource_type == "AWS::Serverless::Api":
638-
stage_id = f"{resource_id}Stage"
639-
if stage_id not in resources:
640-
try:
641-
resources[stage_id] = Resource({"Type": "AWS::ApiGateway::Stage"})
642-
except ValueError:
643-
pass
675+
_inject(resources, f"{resource_id}Stage", "AWS::ApiGateway::Stage")
676+
if "Domain" in props:
677+
_inject(
678+
resources,
679+
f"{resource_id}DomainName",
680+
"AWS::ApiGateway::DomainName",
681+
)
682+
if "Auth" in props:
683+
_inject(
684+
resources,
685+
f"{resource_id}UsagePlan",
686+
"AWS::ApiGateway::UsagePlan",
687+
)
644688

645689
if resource_type == "AWS::Serverless::HttpApi":
646-
stage_id = f"{resource_id}Stage"
647-
if stage_id not in resources:
648-
try:
649-
resources[stage_id] = Resource({"Type": "AWS::ApiGatewayV2::Stage"})
650-
except ValueError:
651-
pass
652-
653-
if resource_type != "AWS::Serverless::Function":
654-
continue
690+
_inject(resources, f"{resource_id}Stage", "AWS::ApiGatewayV2::Stage")
655691

656-
events = props.get("Events", {})
657-
if not isinstance(events, dict):
658-
continue
659-
for event in events.values():
660-
if not isinstance(event, dict):
661-
continue
662-
event_type = event.get("Type")
663-
if event_type == "Api":
664-
event_props = event.get("Properties", {})
665-
if not isinstance(event_props, dict) or "RestApiId" not in event_props:
666-
needs_rest_api = True
667-
elif event_type == "HttpApi":
668-
event_props = event.get("Properties", {})
669-
if not isinstance(event_props, dict) or "ApiId" not in event_props:
670-
needs_http_api = True
671-
672-
if needs_rest_api and "ServerlessRestApi" not in resources:
673-
try:
674-
resources["ServerlessRestApi"] = Resource({"Type": "AWS::Serverless::Api"})
675-
except ValueError:
676-
pass
692+
if needs_rest_api:
693+
_inject(resources, "ServerlessRestApi", "AWS::Serverless::Api")
694+
_inject(resources, "ServerlessRestApiStage", "AWS::ApiGateway::Stage")
677695

678-
if needs_http_api and "ServerlessHttpApi" not in resources:
679-
try:
680-
resources["ServerlessHttpApi"] = Resource(
681-
{"Type": "AWS::Serverless::HttpApi"}
682-
)
683-
except ValueError:
684-
pass
696+
if needs_http_api:
697+
_inject(resources, "ServerlessHttpApi", "AWS::Serverless::HttpApi")
698+
_inject(resources, "ServerlessHttpApiStage", "AWS::ApiGatewayV2::Stage")
685699

686700

687701
def create_context_for_template(

0 commit comments

Comments
 (0)