-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: raise on ambiguous resource ID collision across sibling nested stacks #9186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
5ed83e7
1ac9443
4dce905
383d8f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,10 @@ | |
| from dataclasses import dataclass | ||
| from enum import Enum | ||
| from pathlib import Path | ||
| from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Set, Union, cast | ||
| from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Set, Tuple, Union, cast | ||
|
|
||
| from samcli.commands.local.cli_common.user_exceptions import ( | ||
| AmbiguousResourceIdentifier, | ||
| InvalidFunctionPropertyType, | ||
| InvalidLayerVersionArn, | ||
| UnsupportedIntrinsic, | ||
|
|
@@ -883,22 +884,52 @@ def get_resource_by_id( | |
| ------- | ||
| Dict | ||
| Resource dict | ||
|
|
||
| Raises | ||
| ------ | ||
| AmbiguousResourceIdentifier | ||
| If a bare logical ID (no stack path given) matches resources in more than one *nested* | ||
| stack, with no match in the root stack to prefer. The root stack, if it has a match, always | ||
| takes priority (this is relied upon by existing behavior); but silently picking the first | ||
| nested-stack match in stack-list order when there is no root match - and more than one | ||
| nested stack collides - could operate on the wrong physical resource (e.g. | ||
| `sam sync --resource-id`), so that specific case is now a clear, actionable error instead. | ||
| """ | ||
| search_all_stacks = not identifier.stack_path and not explicit_nested | ||
| matches: List[Tuple[Stack, str, Dict[str, Any]]] = [] | ||
| for stack in stacks: | ||
| if stack.stack_path == identifier.stack_path or search_all_stacks: | ||
| found_resource = None | ||
| found_resource_id = None | ||
| for logical_id, resource in stack.resources.items(): | ||
| resource_id = ResourceMetadataNormalizer.get_resource_id(resource, logical_id) | ||
| if resource_id == identifier.resource_iac_id or ( | ||
| not identifier.stack_path and logical_id == identifier.resource_iac_id | ||
| ): | ||
| found_resource = resource | ||
| found_resource_id = resource_id | ||
| break | ||
|
|
||
| if found_resource: | ||
| return cast(Dict[str, Any], found_resource) | ||
| return None | ||
| if not stack.stack_path: | ||
| # The root stack always takes priority over nested stacks. | ||
| return cast(Dict[str, Any], found_resource) | ||
| matches.append((stack, cast(str, found_resource_id), found_resource)) | ||
| if not search_all_stacks: | ||
| break | ||
|
|
||
| if len(matches) > 1: | ||
| # Build the remediation paths from the *matched* (normalized) resource ID, not the raw | ||
| # identifier the user typed: for CDK/SamResourceId resources the two can differ, and a | ||
| # path built from the raw logical ID would never resolve on retry. | ||
| colliding_paths = [get_full_path(stack.stack_path, resource_id) for stack, resource_id, _ in matches] | ||
| raise AmbiguousResourceIdentifier( | ||
| f"Resource ID '{identifier.resource_iac_id}' is ambiguous: it matches resources in more than one " | ||
| f"nested stack ({', '.join(colliding_paths)}). Qualify it with the full stack path, " | ||
| f"e.g. '{colliding_paths[0]}'." | ||
| ) | ||
|
|
||
| return cast(Dict[str, Any], matches[0][2]) if matches else None | ||
|
|
||
|
|
||
| def get_resource_full_path_by_id(stacks: List[Stack], identifier: ResourceIdentifier) -> Optional[str]: | ||
|
|
@@ -915,7 +946,16 @@ def get_resource_full_path_by_id(stacks: List[Stack], identifier: ResourceIdenti | |
| ------- | ||
| str | ||
| return resource full path | ||
|
|
||
| Raises | ||
| ------ | ||
| AmbiguousResourceIdentifier | ||
| If a bare logical ID (no stack path given) matches resources in more than one *nested* | ||
| stack, with no match in the root stack to prefer. Mirrors the precedence/ambiguity rule | ||
| enforced by get_resource_by_id, so the two entry points cannot disagree on what a bare ID | ||
| resolves to. | ||
| """ | ||
| matches: List[str] = [] | ||
| for stack in stacks: | ||
| if identifier.stack_path and identifier.stack_path != stack.stack_path: | ||
| continue | ||
|
|
@@ -924,8 +964,19 @@ def get_resource_full_path_by_id(stacks: List[Stack], identifier: ResourceIdenti | |
| if resource_id == identifier.resource_iac_id or ( | ||
| not identifier.stack_path and logical_id == identifier.resource_iac_id | ||
| ): | ||
| return get_full_path(stack.stack_path, resource_id) | ||
| return None | ||
| if not stack.stack_path: | ||
| # The root stack always takes priority over nested stacks. | ||
| return get_full_path(stack.stack_path, resource_id) | ||
| matches.append(get_full_path(stack.stack_path, resource_id)) | ||
| break | ||
|
|
||
| if len(matches) > 1: | ||
| raise AmbiguousResourceIdentifier( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [GENERAL] Raising from # samcli/commands/package/package_context.py:140
# samcli/commands/deploy/guided_context.py:358
repo_full_path = get_resource_full_path_by_id(stacks, ResourceIdentifier(image_repo_func_id))
if repo_full_path:
updated_repo[repo_full_path] = image_repo_uri
So for a template with two sibling nested stacks each containing Erroring here is defensible (mapping an image repo to the wrong function is the same class of hazard as syncing the wrong function), but it is a user-facing behavior change for deploy/package, not just for the sync --resource-id path the PR describes. Worth either scoping the hard error to the sync resolution path or calling the deploy/package impact out explicitly in the PR description/changelog so it isn't discovered as a regression. |
||
| f"Resource ID '{identifier.resource_iac_id}' is ambiguous: it matches resources in more than one " | ||
| f"nested stack ({', '.join(matches)}). Qualify it with the full stack path, e.g. '{matches[0]}'." | ||
| ) | ||
|
|
||
| return matches[0] if matches else None | ||
|
|
||
|
|
||
| def get_resource_ids_by_type(stacks: List[Stack], resource_type: str) -> List[ResourceIdentifier]: | ||
|
|
@@ -1045,8 +1096,7 @@ def get_function_build_info( | |
| loadable = imageuri and check_path_valid_type(imageuri) and Path(imageuri).is_file() | ||
| if not buildable and not loadable: | ||
| LOG.debug( | ||
| "Skip Building %s function, as it is missing either Dockerfile or DockerContext " | ||
| "metadata properties.", | ||
| "Skip Building %s function, as it is missing either Dockerfile or DockerContext metadata properties.", | ||
| full_path, | ||
| ) | ||
| return FunctionBuildInfo.NonBuildableImage | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[GENERAL] The same "bare ID matches resources in more than one stack" condition is already handled elsewhere in the codebase with the opposite strategy — warn and pick deterministically (
samcli/lib/providers/sam_function_provider.py:139-160):After this change the two resolvers disagree on the same user input:
sam sync --resource-id Function1hard-fails, whilesam local invoke Function1still silently picks the alphabetically-first full path with a warning. That matters for two reasons: the "operates on the wrong resource" hazard motivating this PR is only closed on one of the two resolution paths, and future maintainers now have two contradictory precedence rules to reason about (first-nested-match is an error here; lowest-sorted-full-path wins there).Either align the two (reuse the warn-and-pick behavior, or extend the ambiguity error to
SamFunctionProvider.get), or add a short comment next to thisraiseexplaining why sync-style resolution must be stricter than invoke-style resolution, so the divergence is a documented decision rather than an accident.