localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. localstack/aws/api/cloudformation/__init__.py +1 -0
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +4 -0
  5. localstack/aws/api/ec2/__init__.py +1113 -56
  6. localstack/aws/api/iam/__init__.py +7 -0
  7. localstack/aws/api/kinesis/__init__.py +19 -0
  8. localstack/aws/api/kms/__init__.py +6 -0
  9. localstack/aws/api/lambda_/__init__.py +13 -0
  10. localstack/aws/api/logs/__init__.py +15 -0
  11. localstack/aws/api/redshift/__init__.py +9 -3
  12. localstack/aws/api/route53/__init__.py +2 -0
  13. localstack/aws/api/s3/__init__.py +12 -0
  14. localstack/aws/api/s3control/__init__.py +32 -0
  15. localstack/aws/api/ssm/__init__.py +2 -0
  16. localstack/aws/client.py +7 -2
  17. localstack/aws/forwarder.py +52 -5
  18. localstack/aws/handlers/analytics.py +1 -1
  19. localstack/aws/handlers/logging.py +12 -2
  20. localstack/aws/handlers/metric_handler.py +41 -1
  21. localstack/aws/handlers/service.py +32 -9
  22. localstack/aws/protocol/parser.py +440 -21
  23. localstack/aws/protocol/serializer.py +684 -64
  24. localstack/aws/protocol/service_router.py +120 -20
  25. localstack/aws/skeleton.py +4 -2
  26. localstack/aws/spec-patches.json +58 -0
  27. localstack/aws/spec.py +33 -13
  28. localstack/cli/exceptions.py +1 -1
  29. localstack/cli/localstack.py +4 -4
  30. localstack/cli/lpm.py +3 -4
  31. localstack/cli/profiles.py +1 -2
  32. localstack/config.py +18 -12
  33. localstack/constants.py +4 -29
  34. localstack/dev/kubernetes/__main__.py +1 -1
  35. localstack/dev/run/paths.py +1 -1
  36. localstack/dns/plugins.py +5 -1
  37. localstack/dns/server.py +12 -3
  38. localstack/packages/api.py +9 -8
  39. localstack/packages/core.py +2 -2
  40. localstack/packages/plugins.py +0 -8
  41. localstack/runtime/init.py +1 -1
  42. localstack/services/apigateway/legacy/provider.py +53 -3
  43. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  44. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  45. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  46. localstack/services/apigateway/next_gen/provider.py +5 -0
  47. localstack/services/cloudformation/engine/entities.py +12 -1
  48. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  49. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  50. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  52. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  53. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  54. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  55. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  56. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  57. localstack/services/cloudformation/resource_provider.py +5 -1
  58. localstack/services/cloudformation/resources.py +24149 -0
  59. localstack/services/cloudformation/v2/entities.py +6 -3
  60. localstack/services/cloudformation/v2/provider.py +172 -27
  61. localstack/services/cloudformation/v2/types.py +8 -4
  62. localstack/services/cloudwatch/provider_v2.py +25 -28
  63. localstack/services/dynamodb/packages.py +2 -1
  64. localstack/services/dynamodb/provider.py +42 -0
  65. localstack/services/dynamodb/v2/provider.py +42 -0
  66. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  67. localstack/services/es/provider.py +2 -2
  68. localstack/services/events/event_rule_engine.py +31 -13
  69. localstack/services/events/models.py +4 -5
  70. localstack/services/events/target.py +17 -9
  71. localstack/services/iam/provider.py +11 -116
  72. localstack/services/iam/resources/policy_simulator.py +133 -0
  73. localstack/services/kinesis/models.py +15 -2
  74. localstack/services/kinesis/provider.py +77 -0
  75. localstack/services/kms/provider.py +14 -5
  76. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  77. localstack/services/lambda_/packages.py +1 -1
  78. localstack/services/logs/provider.py +1 -1
  79. localstack/services/moto.py +2 -1
  80. localstack/services/opensearch/cluster.py +15 -7
  81. localstack/services/opensearch/packages.py +26 -7
  82. localstack/services/opensearch/provider.py +6 -1
  83. localstack/services/opensearch/versions.py +56 -7
  84. localstack/services/s3/constants.py +5 -2
  85. localstack/services/s3/cors.py +4 -4
  86. localstack/services/s3/notifications.py +1 -1
  87. localstack/services/s3/presigned_url.py +27 -43
  88. localstack/services/s3/provider.py +67 -11
  89. localstack/services/s3/utils.py +42 -11
  90. localstack/services/ses/provider.py +16 -7
  91. localstack/services/sns/constants.py +7 -1
  92. localstack/services/sns/v2/models.py +167 -0
  93. localstack/services/sns/v2/provider.py +860 -2
  94. localstack/services/sns/v2/utils.py +130 -0
  95. localstack/services/sqs/developer_api.py +205 -0
  96. localstack/services/sqs/models.py +42 -3
  97. localstack/services/sqs/provider.py +8 -309
  98. localstack/services/sqs/query_api.py +1 -1
  99. localstack/services/sqs/utils.py +121 -2
  100. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  101. localstack/testing/aws/cloudformation_utils.py +1 -1
  102. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  103. localstack/testing/pytest/container.py +4 -5
  104. localstack/testing/pytest/fixtures.py +20 -19
  105. localstack/testing/pytest/in_memory_localstack.py +0 -4
  106. localstack/testing/pytest/marking.py +13 -4
  107. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  108. localstack/testing/pytest/util.py +1 -1
  109. localstack/testing/pytest/validation_tracking.py +1 -2
  110. localstack/testing/snapshots/transformer_utility.py +5 -0
  111. localstack/utils/analytics/events.py +2 -2
  112. localstack/utils/analytics/metadata.py +1 -2
  113. localstack/utils/analytics/metrics/counter.py +6 -8
  114. localstack/utils/analytics/publisher.py +1 -2
  115. localstack/utils/analytics/service_request_aggregator.py +2 -2
  116. localstack/utils/archives.py +11 -11
  117. localstack/utils/aws/arns.py +17 -9
  118. localstack/utils/aws/aws_responses.py +7 -7
  119. localstack/utils/aws/aws_stack.py +2 -3
  120. localstack/utils/aws/message_forwarding.py +1 -2
  121. localstack/utils/aws/request_context.py +4 -5
  122. localstack/utils/batch_policy.py +3 -3
  123. localstack/utils/bootstrap.py +7 -7
  124. localstack/utils/catalog/catalog.py +139 -0
  125. localstack/utils/catalog/catalog_loader.py +11 -0
  126. localstack/utils/catalog/common.py +58 -0
  127. localstack/utils/catalog/plugins.py +28 -0
  128. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  129. localstack/utils/collections.py +7 -8
  130. localstack/utils/config_listener.py +1 -1
  131. localstack/utils/container_networking.py +2 -3
  132. localstack/utils/container_utils/container_client.py +115 -131
  133. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  134. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  135. localstack/utils/diagnose.py +2 -3
  136. localstack/utils/docker_utils.py +3 -4
  137. localstack/utils/files.py +31 -7
  138. localstack/utils/functions.py +3 -2
  139. localstack/utils/http.py +4 -5
  140. localstack/utils/json.py +19 -5
  141. localstack/utils/kinesis/kinesis_connector.py +2 -1
  142. localstack/utils/net.py +6 -6
  143. localstack/utils/no_exit_argument_parser.py +2 -2
  144. localstack/utils/numbers.py +9 -2
  145. localstack/utils/objects.py +6 -5
  146. localstack/utils/patch.py +2 -1
  147. localstack/utils/run.py +10 -9
  148. localstack/utils/scheduler.py +11 -11
  149. localstack/utils/server/tcp_proxy.py +2 -2
  150. localstack/utils/serving.py +2 -3
  151. localstack/utils/strings.py +10 -11
  152. localstack/utils/sync.py +126 -1
  153. localstack/utils/tagging.py +1 -4
  154. localstack/utils/testutil.py +5 -4
  155. localstack/utils/threads.py +2 -2
  156. localstack/utils/time.py +11 -3
  157. localstack/utils/urls.py +1 -3
  158. localstack/version.py +2 -2
  159. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
  160. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
  161. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
  162. localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
  163. localstack/packages/terraform.py +0 -46
  164. localstack/services/cloudformation/deploy.html +0 -144
  165. localstack/services/cloudformation/deploy_ui.py +0 -47
  166. localstack/services/cloudformation/plugins.py +0 -12
  167. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  168. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
  169. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
  170. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
  171. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
  172. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
  173. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
@@ -30,6 +30,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
30
30
  NodeProperties,
31
31
  NodeProperty,
32
32
  NodeResource,
33
+ NodeResources,
33
34
  NodeTemplate,
34
35
  Nothing,
35
36
  NothingType,
@@ -45,6 +46,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model_visitor impor
45
46
  ChangeSetModelVisitor,
46
47
  )
47
48
  from localstack.services.cloudformation.engine.v2.resolving import (
49
+ REGEX_DYNAMIC_REF,
48
50
  extract_dynamic_reference,
49
51
  perform_dynamic_reference_lookup,
50
52
  )
@@ -55,6 +57,7 @@ from localstack.services.cloudformation.stores import (
55
57
  from localstack.services.cloudformation.v2.entities import ChangeSet
56
58
  from localstack.services.cloudformation.v2.types import ResolvedResource
57
59
  from localstack.utils.aws.arns import get_partition
60
+ from localstack.utils.numbers import to_number
58
61
  from localstack.utils.objects import get_value_from_path
59
62
  from localstack.utils.run import to_str
60
63
  from localstack.utils.strings import to_bytes
@@ -224,6 +227,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
224
227
  def process(self) -> None:
225
228
  self._setup_runtime_cache()
226
229
  node_template = self._change_set.update_model.node_template
230
+ node_conditions = self._change_set.update_model.node_template.conditions
231
+ self.visit(node_conditions)
227
232
  self.visit(node_template)
228
233
  self._save_runtime_cache()
229
234
 
@@ -273,10 +278,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
273
278
  property_value: Any | None = get_value_from_path(properties, property_name)
274
279
 
275
280
  if property_value:
276
- if not isinstance(property_value, (str, list)):
277
- # TODO: is this correct? If there is a bug in the logic here, it's probably
278
- # better to know about it with a clear error message than to receive some form
279
- # of message about trying to use a dictionary in place of a string
281
+ if not isinstance(property_value, (str, list, dict)):
282
+ # Str: Standard expected type. TODO validate bools and numbers
283
+ # List: Multiple resource types can return a list of values e.g. AWS::EC2::VPC.
284
+ # Dict: Custom resources in CloudFormation can return arbitrary data structures.
280
285
  raise RuntimeError(
281
286
  f"Accessing property '{property_name}' from '{resource_logical_id}' resulted in a non-string value nor list"
282
287
  )
@@ -432,6 +437,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
432
437
  def _perform_dynamic_replacements(self, value: _T) -> _T:
433
438
  if not isinstance(value, str):
434
439
  return value
440
+
435
441
  if dynamic_ref := extract_dynamic_reference(value):
436
442
  new_value = perform_dynamic_reference_lookup(
437
443
  reference=dynamic_ref,
@@ -439,7 +445,11 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
439
445
  region_name=self._change_set.region_name,
440
446
  )
441
447
  if new_value:
442
- return new_value
448
+ # We need to use a function here, to avoid backslash processing by regex.
449
+ # From the regex sub documentation:
450
+ # repl can be a string or a function; if it is a string, any backslash escapes in it are processed.
451
+ # Using a function, we can avoid this processing.
452
+ return REGEX_DYNAMIC_REF.sub(lambda _: new_value, value)
443
453
 
444
454
  return value
445
455
 
@@ -559,13 +569,49 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
559
569
  arguments_list = arguments.split(".")
560
570
  else:
561
571
  arguments_list = arguments
572
+
573
+ if len(arguments_list) < 2:
574
+ raise ValidationError(
575
+ "Template error: every Fn::GetAtt object requires two non-empty parameters, the resource name and the resource attribute"
576
+ )
577
+
562
578
  logical_name_of_resource = arguments_list[0]
563
- attribute_name = arguments_list[1]
579
+ attribute_name = ".".join(arguments_list[1:])
564
580
 
565
581
  node_resource = self._get_node_resource_for(
566
582
  resource_name=logical_name_of_resource,
567
583
  node_template=self._change_set.update_model.node_template,
568
584
  )
585
+
586
+ if not is_nothing(node_resource.condition_reference):
587
+ condition = self._get_node_condition_if_exists(node_resource.condition_reference.value)
588
+ evaluation_result = self._resolve_condition(condition.name)
589
+
590
+ if select_before and not evaluation_result.before:
591
+ raise ValidationError(
592
+ f"Template format error: Unresolved resource dependencies [{logical_name_of_resource}] in the Resources block of the template"
593
+ )
594
+
595
+ if not select_before and not evaluation_result.after:
596
+ raise ValidationError(
597
+ f"Template format error: Unresolved resource dependencies [{logical_name_of_resource}] in the Resources block of the template"
598
+ )
599
+
600
+ # Custom Resources can mutate their definition
601
+ # So the preproc should search first in the resource values and then check the template
602
+ if select_before:
603
+ value = self._before_deployed_property_value_of(
604
+ resource_logical_id=logical_name_of_resource,
605
+ property_name=attribute_name,
606
+ )
607
+ else:
608
+ value = self._after_deployed_property_value_of(
609
+ resource_logical_id=logical_name_of_resource,
610
+ property_name=attribute_name,
611
+ )
612
+ if value is not None:
613
+ return value
614
+
569
615
  node_property: NodeProperty | None = self._get_node_property_for(
570
616
  property_name=attribute_name, node_resource=node_resource
571
617
  )
@@ -573,19 +619,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
573
619
  # The property is statically defined in the template and its value can be computed.
574
620
  property_delta = self.visit(node_property)
575
621
  value = property_delta.before if select_before else property_delta.after
576
- else:
577
- # The property is not statically defined and must therefore be available in
578
- # the properties deployed set.
579
- if select_before:
580
- value = self._before_deployed_property_value_of(
581
- resource_logical_id=logical_name_of_resource,
582
- property_name=attribute_name,
583
- )
584
- else:
585
- value = self._after_deployed_property_value_of(
586
- resource_logical_id=logical_name_of_resource,
587
- property_name=attribute_name,
588
- )
622
+
589
623
  return value
590
624
 
591
625
  def visit_node_intrinsic_function_fn_get_att(
@@ -614,6 +648,12 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
614
648
  return args[0] == args[1]
615
649
 
616
650
  arguments_delta = self.visit(node_intrinsic_function.arguments)
651
+
652
+ if isinstance(arguments_delta.after, list) and len(arguments_delta.after) != 2:
653
+ raise ValidationError(
654
+ "Template error: every Fn::Equals object requires a list of 2 string parameters."
655
+ )
656
+
617
657
  delta = self._cached_apply(
618
658
  scope=node_intrinsic_function.scope,
619
659
  arguments_delta=arguments_delta,
@@ -843,13 +883,27 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
843
883
  ):
844
884
  # TODO: add further support for schema validation
845
885
  def _compute_fn_select(args: list[Any]) -> Any:
846
- values: list[Any] = args[1]
886
+ values = args[1]
887
+ # defer evaluation if the selection list contains unresolved elements (e.g., unresolved intrinsics)
888
+ if isinstance(values, list) and not all(isinstance(value, str) for value in values):
889
+ raise RuntimeError("Fn::Select list contains unresolved elements")
890
+
847
891
  if not isinstance(values, list) or not values:
848
- raise RuntimeError(f"Invalid arguments list value for Fn::Select: '{values}'")
892
+ raise ValidationError(
893
+ "Template error: Fn::Select requires a list argument with two elements: an integer index and a list"
894
+ )
895
+ try:
896
+ index: int = int(args[0])
897
+ except ValueError as e:
898
+ raise ValidationError(
899
+ "Template error: Fn::Select requires a list argument with two elements: an integer index and a list"
900
+ ) from e
901
+
849
902
  values_len = len(values)
850
- index: int = int(args[0])
851
- if not isinstance(index, int) or index < 0 or index > values_len:
852
- raise RuntimeError(f"Invalid or out of range index value for Fn::Select: '{index}'")
903
+ if index < 0 or index >= values_len:
904
+ raise ValidationError(
905
+ "Template error: Fn::Select requires a list argument with two elements: an integer index and a list"
906
+ )
853
907
  selection = values[index]
854
908
  return selection
855
909
 
@@ -876,6 +930,17 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
876
930
  return split_string
877
931
 
878
932
  arguments_delta = self.visit(node_intrinsic_function.arguments)
933
+
934
+ if not (
935
+ is_nothing(arguments_delta.after)
936
+ or isinstance(arguments_delta.after, list)
937
+ and len(arguments_delta.after) == 2
938
+ ):
939
+ raise ValidationError(
940
+ "Template error: every Fn::Split object requires two parameters, "
941
+ "(1) a string delimiter and (2) a string to be split or a function that returns a string to be split."
942
+ )
943
+
879
944
  delta = self._cached_apply(
880
945
  scope=node_intrinsic_function.scope,
881
946
  arguments_delta=arguments_delta,
@@ -975,6 +1040,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
975
1040
  return PreprocEntityDelta(before=before_parameters, after=after_parameters)
976
1041
 
977
1042
  def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta:
1043
+ if not VALID_LOGICAL_RESOURCE_ID_RE.match(node_parameter.name):
1044
+ raise ValidationError(
1045
+ f"Template format error: Parameter name {node_parameter.name} is non alphanumeric."
1046
+ )
978
1047
  dynamic_value = node_parameter.dynamic_value
979
1048
  dynamic_delta = self.visit(dynamic_value)
980
1049
 
@@ -990,6 +1059,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
990
1059
  match type_:
991
1060
  case "List<String>" | "CommaDelimitedList":
992
1061
  return [item.strip() for item in value.split(",")]
1062
+ case "Number":
1063
+ # TODO: validate the parameter type at template parse time (or whatever is in parity with AWS) so we know this cannot fail
1064
+ return to_number(value)
993
1065
  return value
994
1066
 
995
1067
  if not is_nothing(after):
@@ -1131,6 +1203,20 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1131
1203
  after = after_delta.after
1132
1204
  return PreprocEntityDelta(before=before, after=after)
1133
1205
 
1206
+ def visit_node_resources(self, node_resources: NodeResources):
1207
+ """
1208
+ Skip resources where they conditionally evaluate to False
1209
+ """
1210
+ for node_resource in node_resources.resources:
1211
+ if not is_nothing(node_resource.condition_reference):
1212
+ condition_delta = self._resolve_resource_condition_reference(
1213
+ node_resource.condition_reference
1214
+ )
1215
+ condition_after = condition_delta.after
1216
+ if condition_after is False:
1217
+ continue
1218
+ self.visit(node_resource)
1219
+
1134
1220
  def visit_node_resource(
1135
1221
  self, node_resource: NodeResource
1136
1222
  ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
@@ -1241,6 +1327,14 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1241
1327
  before: list[PreprocOutput] = []
1242
1328
  after: list[PreprocOutput] = []
1243
1329
  for node_output in node_outputs.outputs:
1330
+ if not is_nothing(node_output.condition_reference):
1331
+ condition_delta = self._resolve_resource_condition_reference(
1332
+ node_output.condition_reference
1333
+ )
1334
+ condition_after = condition_delta.after
1335
+ if condition_after is False:
1336
+ continue
1337
+
1244
1338
  output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
1245
1339
  output_before = output_delta.before
1246
1340
  output_after = output_delta.after
@@ -501,7 +501,10 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
501
501
  def visit_node_intrinsic_function_fn_get_att(
502
502
  self, node_intrinsic_function: NodeIntrinsicFunction
503
503
  ) -> PreprocEntityDelta:
504
- return self.visit(node_intrinsic_function.arguments)
504
+ try:
505
+ return super().visit_node_intrinsic_function_fn_get_att(node_intrinsic_function)
506
+ except RuntimeError:
507
+ return self.visit(node_intrinsic_function.arguments)
505
508
 
506
509
  def visit_node_intrinsic_function_fn_sub(
507
510
  self, node_intrinsic_function: NodeIntrinsicFunction
@@ -4,7 +4,6 @@ from typing import Any
4
4
  from botocore.exceptions import ParamValidationError
5
5
 
6
6
  from localstack.services.cloudformation.engine.v2.change_set_model import (
7
- Maybe,
8
7
  NodeIntrinsicFunction,
9
8
  NodeProperty,
10
9
  NodeResource,
@@ -27,23 +26,15 @@ class ChangeSetModelValidator(ChangeSetModelPreproc):
27
26
  def visit_node_template(self, node_template: NodeTemplate):
28
27
  self.visit(node_template.mappings)
29
28
  self.visit(node_template.resources)
29
+ self.visit(node_template.parameters)
30
30
 
31
31
  def visit_node_intrinsic_function_fn_get_att(
32
32
  self, node_intrinsic_function: NodeIntrinsicFunction
33
33
  ) -> PreprocEntityDelta:
34
- arguments_delta = self.visit(node_intrinsic_function.arguments)
35
- before_arguments: Maybe[str | list[str]] = arguments_delta.before
36
- after_arguments: Maybe[str | list[str]] = arguments_delta.after
37
-
38
- before = self._before_cache.get(node_intrinsic_function.scope, Nothing)
39
- if is_nothing(before) and not is_nothing(before_arguments):
40
- before = ".".join(before_arguments)
41
-
42
- after = self._after_cache.get(node_intrinsic_function.scope, Nothing)
43
- if is_nothing(after) and not is_nothing(after_arguments):
44
- after = ".".join(after_arguments)
45
-
46
- return PreprocEntityDelta(before=before, after=after)
34
+ try:
35
+ return super().visit_node_intrinsic_function_fn_get_att(node_intrinsic_function)
36
+ except RuntimeError:
37
+ return self.visit(node_intrinsic_function.arguments)
47
38
 
48
39
  def visit_node_intrinsic_function_fn_sub(
49
40
  self, node_intrinsic_function: NodeIntrinsicFunction
@@ -54,6 +54,7 @@ class ChangeSetModelVisitor(abc.ABC):
54
54
  # entities (parameters, mappings, conditions, etc.). Then compute the output fields; computing
55
55
  # only the output fields would only result in the deployment logic of the referenced outputs
56
56
  # being evaluated, hence enforce the visiting of all the resources first.
57
+ self.visit(node_template.conditions)
57
58
  self.visit(node_template.resources)
58
59
  self.visit(node_template.outputs)
59
60
 
@@ -10,7 +10,9 @@ from localstack.aws.connect import connect_to
10
10
 
11
11
  LOG = logging.getLogger(__name__)
12
12
 
13
- REGEX_DYNAMIC_REF = re.compile(r"{{resolve:([^:]+):(.+)}}")
13
+ # CloudFormation allows using dynamic references in `Fn::Sub` expressions, so we must make sure
14
+ # we don't capture the parameter usage by excluding ${} characters
15
+ REGEX_DYNAMIC_REF = re.compile(r"{{resolve:([^:]+):([^${}]+)}}")
14
16
 
15
17
 
16
18
  @dataclass
@@ -21,7 +23,7 @@ class DynamicReference:
21
23
 
22
24
  def extract_dynamic_reference(value: Any) -> DynamicReference | None:
23
25
  if isinstance(value, str):
24
- if dynamic_ref_match := REGEX_DYNAMIC_REF.match(value):
26
+ if dynamic_ref_match := REGEX_DYNAMIC_REF.search(value):
25
27
  return DynamicReference(dynamic_ref_match[1], dynamic_ref_match[2])
26
28
  return None
27
29
 
@@ -90,9 +92,9 @@ def perform_dynamic_reference_lookup(
90
92
  raise RuntimeError(
91
93
  f"JSON value for {reference.service_name}.{reference.reference_key} not present"
92
94
  )
93
- return json_secret[json_key]
95
+ return str(json_secret[json_key])
94
96
  else:
95
- return secret_value
97
+ return str(secret_value)
96
98
 
97
99
  LOG.warning(
98
100
  "Unsupported service for dynamic parameter: service_name=%s", reference.service_name
@@ -1,5 +1,7 @@
1
1
  import yaml
2
2
 
3
+ from localstack.services.cloudformation.engine.validations import ValidationError
4
+
3
5
 
4
6
  def construct_raw(_, node):
5
7
  return node.value
@@ -60,5 +62,10 @@ customloader = NoDatesSafeLoader
60
62
  yaml.add_multi_constructor("!", shorthand_constructor, customloader)
61
63
 
62
64
 
63
- def parse_yaml(input_data: str):
64
- return yaml.load(input_data, customloader)
65
+ def parse_yaml(input_data: str) -> dict:
66
+ parsed = yaml.load(input_data, Loader=customloader)
67
+
68
+ if not isinstance(parsed, dict):
69
+ raise ValidationError("Template format error: unsupported structure.")
70
+
71
+ return parsed
@@ -518,10 +518,14 @@ class ResourceProviderExecutor:
518
518
  try:
519
519
  return resource_provider.update(request)
520
520
  except NotImplementedError:
521
+ feature_request_url = "https://github.com/localstack/localstack/issues/new?template=feature-request.yml"
521
522
  LOG.warning(
522
- 'Unable to update resource type "%s", id "%s"',
523
+ 'Unable to update resource type "%s", id "%s", '
524
+ "the update operation is not implemented for this resource. "
525
+ "Please consider submitting a feature request at this URL: %s",
523
526
  request.resource_type,
524
527
  request.logical_resource_id,
528
+ feature_request_url,
525
529
  )
526
530
  if request.previous_state is None:
527
531
  # this is an issue with our update detection. We should never be in this state.