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
25 changes: 16 additions & 9 deletions localstack/services/cloudformation/engine/template_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from localstack.utils.strings import to_bytes, to_str
from localstack.utils.threads import start_worker_thread

from localstack.services.cloudformation.models import * # noqa: F401, isort:skip
from localstack.services.cloudformation.models import * # noqa: F401, F403, isort:skip
from localstack.utils.urls import localstack_host

ACTION_CREATE = "create"
Expand Down Expand Up @@ -169,7 +169,9 @@ def resolve_ref(
# resource
resource = resources.get(ref)
if not resource:
raise Exception("Should be detected earlier.")
raise Exception(
f"Resource target for `Ref {ref}` could not be found. Is there a resource with name {ref} in your stack?"
)

return resources[ref].get("PhysicalResourceId")

Expand Down Expand Up @@ -325,7 +327,10 @@ def _resolve_refs_recursively(
)
# TODO: we should check the deployment state and not try to GetAtt from a resource that is still IN_PROGRESS or hasn't started yet.
if resolved_getatt is None:
raise DependencyNotYetSatisfied(resource_ids=resource_logical_id, message="")
raise DependencyNotYetSatisfied(
resource_ids=resource_logical_id,
message=f"Could not resolve attribute '{attribute_name}' on resource '{resource_logical_id}'",
)
return resolved_getatt

if stripped_fn_lower == "join":
Expand Down Expand Up @@ -361,7 +366,7 @@ def _resolve_refs_recursively(
none_values = [v for v in join_values if v is None]
if none_values:
raise Exception(
"Cannot resolve CF fn::Join %s due to null values: %s" % (value, join_values)
f"Cannot resolve CF Fn::Join {value} due to null values: {join_values}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we're using the full canonical version while using !Ref above. Would suggest keeping this consistent. (Personal preference would be slightly towards canonical)

)
return value[keys_list[0]][0].join([str(v) for v in join_values])

Expand All @@ -375,7 +380,7 @@ def _resolve_refs_recursively(
item_to_sub[1].update(attr_refs)

for key, val in item_to_sub[1].items():
val = resolve_refs_recursively(
resolved_val = resolve_refs_recursively(
account_id,
region_name,
stack_name,
Expand All @@ -385,11 +390,13 @@ def _resolve_refs_recursively(
parameters,
val,
)
if not isinstance(val, str):
if not isinstance(resolved_val, str):
# We don't have access to the resource that's a dependency in this case,
# so do the best we can with the resource ids
raise DependencyNotYetSatisfied(resource_ids=key, message="")
result = result.replace("${%s}" % key, val)
raise DependencyNotYetSatisfied(
resource_ids=key, message=f"Could not resolve {val} to terminal value type"
)
result = result.replace("${%s}" % key, resolved_val)

# resolve placeholders
result = resolve_placeholders_in_string(
Expand Down Expand Up @@ -1284,7 +1291,7 @@ def do_apply_changes_in_loop(self, changes, stack):
break
if not updated:
raise Exception(
"Resource deployment loop completed, pending resource changes: %s" % changes
f"Resource deployment loop completed, pending resource changes: {changes}"
)

# clean up references to deleted resources in stack
Expand Down
41 changes: 41 additions & 0 deletions tests/aws/services/cloudformation/engine/test_references.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
import os

import pytest

from localstack.testing.pytest import markers
from localstack.testing.pytest.fixtures import StackDeployError
from localstack.utils.files import load_file
from localstack.utils.strings import short_uid

Expand Down Expand Up @@ -53,3 +55,42 @@ def test_fn_sub_cases(self, deploy_cfn_template, aws_client, snapshot):
)

snapshot.match("outputs", deployment.outputs)


@markers.aws.only_localstack
def test_useful_error_when_invalid_ref(deploy_cfn_template):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to add a test here tp avoid us messing it up when refactoring on the future. We might want to pull out tests like this from the file in the future though.

"""
When trying to resolve a non-existent !Ref, make sure the error message includes the name of the !Ref
to clarify which !Ref cannot be resolved.
"""
logical_resource_id = "Topic"
ref_name = "InvalidRef"

template = json.dumps(
{
"Resources": {
logical_resource_id: {
"Type": "AWS::SNS::Topic",
"Properties": {
"Name": {
"Ref": ref_name,
},
},
}
}
}
)

with pytest.raises(StackDeployError) as exc_info:
deploy_cfn_template(template=template)

# get the exception error message from the events list
message = None
for event in exc_info.value.events:
if (
event["LogicalResourceId"] == logical_resource_id
and event["ResourceStatus"] == "CREATE_FAILED"
):
message = event["ResourceStatusReason"]

assert ref_name in message