88from typing_extensions import TypeVar
99
1010from localstack .aws .api .cloudformation import ChangeAction
11+ from localstack .services .cloudformation .resource_provider import ResourceProviderExecutor
1112from localstack .utils .strings import camel_to_snake_case
1213
1314T = 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
372376class 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
0 commit comments