Skip to content

Commit 87d33ca

Browse files
committed
Model requires replacement
1 parent d14ecc7 commit 87d33ca

File tree

3 files changed

+45
-0
lines changed

3 files changed

+45
-0
lines changed

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing_extensions import TypeVar
99

1010
from localstack.aws.api.cloudformation import ChangeAction
11+
from localstack.services.cloudformation.resource_provider import ResourceProviderExecutor
1112
from localstack.utils.strings import camel_to_snake_case
1213

1314
T = TypeVar("T")
@@ -350,6 +351,7 @@ class NodeResource(ChangeSetNode):
350351
properties: Final[NodeProperties]
351352
condition_reference: Final[Maybe[TerminalValue]]
352353
depends_on: Final[Maybe[NodeDependsOn]]
354+
requires_replacement: Final[bool]
353355

354356
def __init__(
355357
self,
@@ -360,13 +362,15 @@ def __init__(
360362
properties: NodeProperties,
361363
condition_reference: Maybe[TerminalValue],
362364
depends_on: Maybe[NodeDependsOn],
365+
requires_replacement: bool,
363366
):
364367
super().__init__(scope=scope, change_type=change_type)
365368
self.name = name
366369
self.type_ = type_
367370
self.properties = properties
368371
self.condition_reference = condition_reference
369372
self.depends_on = depends_on
373+
self.requires_replacement = requires_replacement
370374

371375

372376
class NodeProperties(ChangeSetNode):
@@ -720,6 +724,30 @@ def _resolve_intrinsic_function_fn_if(self, arguments: ChangeSetEntity) -> Chang
720724
change_type = parent_change_type_of([node_condition, *arguments[1:]])
721725
return change_type
722726

727+
def _resolve_requires_replacement(
728+
self, node_properties: NodeProperties, resource_type: TerminalValue
729+
) -> bool:
730+
# a bit hacky but we have to load the resource provider executor _and_ resource provider to get the schema
731+
# Note: we don't log the attempt to load the resource provider, we need to make sure this is only done once and we already do this in the executor
732+
resource_provider = ResourceProviderExecutor.try_load_resource_provider(resource_type.value)
733+
if not resource_provider:
734+
# if we don't support a resource, assume an in-place update for simplicity
735+
return False
736+
737+
create_only_properties: list[str] = resource_provider.SCHEMA.get("createOnlyProperties", [])
738+
# TODO: also hacky: strip the leading `/properties/` string from the definition
739+
# ideally we should use a jsonpath or similar
740+
create_only_properties = [
741+
property.replace("/properties/", "", 1) for property in create_only_properties
742+
]
743+
for node_property in node_properties.properties:
744+
if (
745+
node_property.change_type == ChangeType.MODIFIED
746+
and node_property.name in create_only_properties
747+
):
748+
return True
749+
return False
750+
723751
def _visit_array(
724752
self, scope: Scope, before_array: Maybe[list], after_array: Maybe[list]
725753
) -> NodeArray:
@@ -915,6 +943,9 @@ def _visit_resource(
915943
change_type = change_type_of(
916944
before_resource, after_resource, [properties, condition_reference, depends_on]
917945
)
946+
requires_replacement = self._resolve_requires_replacement(
947+
node_properties=properties, resource_type=terminal_value_type
948+
)
918949
node_resource = NodeResource(
919950
scope=scope,
920951
change_type=change_type,
@@ -923,6 +954,7 @@ def _visit_resource(
923954
properties=properties,
924955
condition_reference=condition_reference,
925956
depends_on=depends_on,
957+
requires_replacement=requires_replacement,
926958
)
927959
self._visited_scopes[scope] = node_resource
928960
return node_resource

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Final, Optional
55

66
import localstack.aws.api.cloudformation as cfn_api
7+
from localstack.aws.api.cloudformation import Replacement
78
from localstack.services.cloudformation.engine.v2.change_set_model import (
89
NodeIntrinsicFunction,
910
NodeProperty,
@@ -118,6 +119,8 @@ def _register_resource_change(
118119
physical_id: Optional[str],
119120
before_properties: Optional[PreprocProperties],
120121
after_properties: Optional[PreprocProperties],
122+
# TODO: remove default
123+
requires_replacement: bool = False,
121124
) -> None:
122125
action = cfn_api.ChangeAction.Modify
123126
if before_properties is None:
@@ -139,6 +142,10 @@ def _register_resource_change(
139142
after_context_properties = {PropertiesKey: after_properties.properties}
140143
after_context_properties_json_str = json.dumps(after_context_properties)
141144
resource_change["AfterContext"] = after_context_properties_json_str
145+
# TODO: handle "Conditional" case
146+
resource_change["Replacement"] = (
147+
Replacement.True_ if requires_replacement else Replacement.False_
148+
)
142149
self._changes.append(
143150
cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change)
144151
)
@@ -159,6 +166,7 @@ def _describe_resource_change(
159166
type_=before.resource_type,
160167
before_properties=before.properties,
161168
after_properties=after.properties,
169+
requires_replacement=after.requires_replacement,
162170
)
163171
# Case: type migration.
164172
# TODO: Add test to assert that on type change the resources are replaced.

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class PreprocResource:
107107
resource_type: str
108108
properties: PreprocProperties
109109
depends_on: Optional[list[str]]
110+
requires_replacement: bool
110111

111112
def __init__(
112113
self,
@@ -116,13 +117,15 @@ def __init__(
116117
resource_type: str,
117118
properties: PreprocProperties,
118119
depends_on: Optional[list[str]],
120+
requires_replacement: bool,
119121
):
120122
self.logical_id = logical_id
121123
self.physical_resource_id = physical_resource_id
122124
self.condition = condition
123125
self.resource_type = resource_type
124126
self.properties = properties
125127
self.depends_on = depends_on
128+
self.requires_replacement = requires_replacement
126129

127130
@staticmethod
128131
def _compare_conditions(c1: bool, c2: bool):
@@ -1127,6 +1130,7 @@ def visit_node_resource(
11271130
resource_type=type_delta.before,
11281131
properties=properties_delta.before,
11291132
depends_on=depends_on_before,
1133+
requires_replacement=False,
11301134
)
11311135
if change_type != ChangeType.REMOVED and is_nothing(condition_after) or condition_after:
11321136
logical_resource_id = node_resource.name
@@ -1143,6 +1147,7 @@ def visit_node_resource(
11431147
resource_type=type_delta.after,
11441148
properties=properties_delta.after,
11451149
depends_on=depends_on_after,
1150+
requires_replacement=node_resource.requires_replacement,
11461151
)
11471152
return PreprocEntityDelta(before=before, after=after)
11481153

0 commit comments

Comments
 (0)