Skip to content

Commit 52b5e58

Browse files
committed
fix(cfn-lang-ext): derive PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES from canonical dicts (#9005)
Replace the literal PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES dict with a derivation from RESOURCES_WITH_LOCAL_PATHS and RESOURCES_WITH_IMAGE_COMPONENT in samcli.lib.utils.resources, establishing a single source of truth. This fixes issue #9005 where eight resource types were missing from the language-extensions packaging path: - AWS::Serverless::Application - AWS::CloudFormation::StackSet - AWS::ElasticBeanstalk::ApplicationVersion - AWS::AppSync::GraphQLSchema - AWS::AppSync::Resolver - AWS::AppSync::FunctionConfiguration - AWS::CloudFormation::ModuleVersion - AWS::CloudFormation::ResourceVersion The derivation also adds AWS::Glue::Job with dotted-path property Command.ScriptLocation, and adds Code.ImageUri to AWS::Lambda::Function. All 14 new tests pass, confirming the derived dict includes the expected entries and excludes design exclusions (ECR::Repository, ServerlessRepo::Application). Cross-task integration anchors for Glue::Job dotted-path handling now pass.
1 parent e314697 commit 52b5e58

2 files changed

Lines changed: 111 additions & 16 deletions

File tree

samcli/lib/cfn_language_extensions/models.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,34 @@ class TemplateProcessingContext:
135135

136136

137137
# Packageable resource types and their artifact properties that can be dynamic in Fn::ForEach blocks.
138-
# These properties reference local files/directories that SAM CLI needs to package.
139-
# Dynamic values (using loop variables) are supported via Mappings transformation.
140-
PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES: Dict[str, List[str]] = {
141-
"AWS::Serverless::Function": ["CodeUri", "ImageUri"],
142-
"AWS::Lambda::Function": ["Code"],
143-
"AWS::Serverless::LayerVersion": ["ContentUri"],
144-
"AWS::Lambda::LayerVersion": ["Content"],
145-
"AWS::Serverless::Api": ["DefinitionUri"],
146-
"AWS::Serverless::HttpApi": ["DefinitionUri"],
147-
"AWS::Serverless::StateMachine": ["DefinitionUri"],
148-
"AWS::Serverless::GraphQLApi": ["SchemaUri", "CodeUri"],
149-
"AWS::ApiGateway::RestApi": ["BodyS3Location"],
150-
"AWS::ApiGatewayV2::Api": ["BodyS3Location"],
151-
"AWS::StepFunctions::StateMachine": ["DefinitionS3Location"],
152-
"AWS::CloudFormation::Stack": ["TemplateURL"],
153-
}
138+
# Derived from samcli.lib.utils.resources to keep one source of truth: any resource type
139+
# whose property the artifact exporter packages MUST be merged back into the original
140+
# (Fn::ForEach-preserving) template. See issue #9005 for the symptom of drift.
141+
#
142+
# Property names follow jmespath syntax (e.g., "Command.ScriptLocation" for nested keys),
143+
# matching the canonical dicts. Consumers must use jmespath-aware get/set when copying.
144+
def _build_packageable_resource_artifact_properties() -> Dict[str, List[str]]:
145+
from samcli.lib.utils.resources import (
146+
AWS_ECR_REPOSITORY,
147+
RESOURCES_WITH_IMAGE_COMPONENT,
148+
RESOURCES_WITH_LOCAL_PATHS,
149+
)
150+
151+
result: Dict[str, List[str]] = {}
152+
for resource_type, props in RESOURCES_WITH_LOCAL_PATHS.items():
153+
result[resource_type] = list(props)
154+
for resource_type, props in RESOURCES_WITH_IMAGE_COMPONENT.items():
155+
# ECR::Repository.RepositoryName is not a packaged-artifact path.
156+
if resource_type == AWS_ECR_REPOSITORY:
157+
continue
158+
result.setdefault(resource_type, [])
159+
for prop in props:
160+
if prop not in result[resource_type]:
161+
result[resource_type].append(prop)
162+
return result
163+
164+
165+
PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES: Dict[str, List[str]] = _build_packageable_resource_artifact_properties()
154166

155167

156168
@dataclass(frozen=True)

tests/unit/lib/cfn_language_extensions/test_models.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,86 @@ def test_template_processing_context_exported(self):
283283

284284
context = TemplateProcessingContext(fragment={})
285285
assert context.fragment == {}
286+
287+
288+
class TestPackageableResourceArtifactProperties:
289+
"""Lock in the derived contents of PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES so future drift fails loudly."""
290+
291+
def test_includes_existing_serverless_function(self):
292+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
293+
assert "CodeUri" in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::Serverless::Function"]
294+
assert "ImageUri" in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::Serverless::Function"]
295+
296+
def test_includes_existing_lambda_function_dotted_image(self):
297+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
298+
props = PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::Lambda::Function"]
299+
assert "Code" in props
300+
assert "Code.ImageUri" in props
301+
302+
def test_includes_serverless_application(self):
303+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
304+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::Serverless::Application"] == ["Location"]
305+
306+
def test_includes_cloudformation_stackset(self):
307+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
308+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::CloudFormation::StackSet"] == ["TemplateURL"]
309+
310+
def test_includes_elasticbeanstalk_application_version(self):
311+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
312+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::ElasticBeanstalk::ApplicationVersion"] == [
313+
"SourceBundle"
314+
]
315+
316+
def test_includes_appsync_graphql_schema(self):
317+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
318+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::AppSync::GraphQLSchema"] == ["DefinitionS3Location"]
319+
320+
def test_includes_appsync_resolver_three_props(self):
321+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
322+
props = PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::AppSync::Resolver"]
323+
assert sorted(props) == sorted(
324+
["RequestMappingTemplateS3Location", "ResponseMappingTemplateS3Location", "CodeS3Location"]
325+
)
326+
327+
def test_includes_appsync_function_configuration_three_props(self):
328+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
329+
props = PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::AppSync::FunctionConfiguration"]
330+
assert sorted(props) == sorted(
331+
["RequestMappingTemplateS3Location", "ResponseMappingTemplateS3Location", "CodeS3Location"]
332+
)
333+
334+
def test_includes_glue_job_dotted_path(self):
335+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
336+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::Glue::Job"] == ["Command.ScriptLocation"]
337+
338+
def test_includes_cloudformation_module_version(self):
339+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
340+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::CloudFormation::ModuleVersion"] == ["ModulePackage"]
341+
342+
def test_includes_cloudformation_resource_version(self):
343+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
344+
assert PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES["AWS::CloudFormation::ResourceVersion"] == [
345+
"SchemaHandlerPackage"
346+
]
347+
348+
def test_excludes_ecr_repository_repositoryname(self):
349+
"""RepositoryName is a name, not a packaged-artifact URI. Excluded by design."""
350+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
351+
assert "AWS::ECR::Repository" not in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
352+
353+
def test_excludes_serverlessrepo_application_metadata(self):
354+
"""ServerlessRepo::Application uses METADATA_WITH_LOCAL_PATHS, not resource props."""
355+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
356+
assert "AWS::ServerlessRepo::Application" not in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
357+
358+
def test_dict_is_in_sync_with_canonical_lists(self):
359+
"""If RESOURCES_WITH_LOCAL_PATHS gains a new entry, this dict must too."""
360+
from samcli.lib.cfn_language_extensions.models import PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES
361+
from samcli.lib.utils.resources import RESOURCES_WITH_LOCAL_PATHS
362+
363+
for resource_type, props in RESOURCES_WITH_LOCAL_PATHS.items():
364+
assert resource_type in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES, (
365+
f"{resource_type} from RESOURCES_WITH_LOCAL_PATHS must be in derived dict"
366+
)
367+
for prop in props:
368+
assert prop in PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES[resource_type]

0 commit comments

Comments
 (0)