Skip to content

Commit 50c35ec

Browse files
committed
Complete SAM implicit resource injection: DeploymentPreference, event permissions, Api Domain/UsagePlan, implicit API stages
1 parent 1f0af07 commit 50c35ec

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
@@ -582,6 +582,17 @@ def _init_transforms(transforms: Any) -> Transforms:
582582
return Transforms([])
583583

584584

585+
def _inject(
586+
resources: dict[str, Resource], logical_id: str, resource_type: str
587+
) -> None:
588+
"""Add a synthetic resource if it doesn't already exist."""
589+
if logical_id not in resources:
590+
try:
591+
resources[logical_id] = Resource({"Type": resource_type})
592+
except ValueError:
593+
pass
594+
595+
585596
def _inject_sam_implicit_resources(
586597
template_resources: Any, resources: dict[str, Resource]
587598
) -> None:
@@ -610,92 +621,95 @@ def _inject_sam_implicit_resources(
610621
"AWS::Serverless::Function",
611622
"AWS::Serverless::StateMachine",
612623
):
613-
role_id = f"{resource_id}Role"
614-
if "Role" not in props and role_id not in resources:
615-
try:
616-
resources[role_id] = Resource({"Type": "AWS::IAM::Role"})
617-
except ValueError:
618-
pass
624+
if "Role" not in props:
625+
_inject(resources, f"{resource_id}Role", "AWS::IAM::Role")
619626

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

659703
if resource_type == "AWS::Serverless::HttpApi":
660-
stage_id = f"{resource_id}Stage"
661-
if stage_id not in resources:
662-
try:
663-
resources[stage_id] = Resource({"Type": "AWS::ApiGatewayV2::Stage"})
664-
except ValueError:
665-
pass
666-
667-
if resource_type != "AWS::Serverless::Function":
668-
continue
704+
_inject(resources, f"{resource_id}Stage", "AWS::ApiGatewayV2::Stage")
669705

670-
events = props.get("Events", {})
671-
if not isinstance(events, dict):
672-
continue
673-
for event in events.values():
674-
if not isinstance(event, dict):
675-
continue
676-
event_type = event.get("Type")
677-
if event_type == "Api":
678-
event_props = event.get("Properties", {})
679-
if not isinstance(event_props, dict) or "RestApiId" not in event_props:
680-
needs_rest_api = True
681-
elif event_type == "HttpApi":
682-
event_props = event.get("Properties", {})
683-
if not isinstance(event_props, dict) or "ApiId" not in event_props:
684-
needs_http_api = True
685-
686-
if needs_rest_api and "ServerlessRestApi" not in resources:
687-
try:
688-
resources["ServerlessRestApi"] = Resource({"Type": "AWS::Serverless::Api"})
689-
except ValueError:
690-
pass
706+
if needs_rest_api:
707+
_inject(resources, "ServerlessRestApi", "AWS::Serverless::Api")
708+
_inject(resources, "ServerlessRestApiStage", "AWS::ApiGateway::Stage")
691709

692-
if needs_http_api and "ServerlessHttpApi" not in resources:
693-
try:
694-
resources["ServerlessHttpApi"] = Resource(
695-
{"Type": "AWS::Serverless::HttpApi"}
696-
)
697-
except ValueError:
698-
pass
710+
if needs_http_api:
711+
_inject(resources, "ServerlessHttpApi", "AWS::Serverless::HttpApi")
712+
_inject(resources, "ServerlessHttpApiStage", "AWS::ApiGatewayV2::Stage")
699713

700714

701715
def create_context_for_template(

0 commit comments

Comments
 (0)