8000 Add support for list-type stack parameters (#8583) · codeperl/localstack@7f3b534 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7f3b534

Browse files
Add support for list-type stack parameters (localstack#8583)
1 parent 35aaedc commit 7f3b534

File tree

6 files changed

+104
-15
lines changed

6 files changed

+104
-15
lines changed

localstack/services/cloudformation/engine/entities.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
from localstack.aws.api.cloudformation import Capability, ChangeSetType, Parameter
55
from localstack.services.cloudformation.engine.parameters import (
6+
StackParameter,
67
convert_stack_parameters_to_list,
78
map_to_legacy_structure,
9+
strip_parameter_type,
810
)
911
from localstack.utils.aws import arns
1012
from localstack.utils.collections import select_attributes
@@ -69,7 +71,7 @@ def __init__(
6971
template = {}
7072

7173
self.resolved_outputs = list() # TODO
72-
self.resolved_parameters: dict[str, Parameter] = {}
74+
self.resolved_parameters: dict[str, StackParameter] = {}
7375

7476
self.metadata = metadata or {}
7577
self.template = template or {}
@@ -106,7 +108,7 @@ def __init__(
106108
self.change_sets = []
107109
# self.evaluated_conditions = {}
108110

109-
def set_resolved_parameters(self, resolved_parameters: dict[str, Parameter]):
111+
def set_resolved_parameters(self, resolved_parameters: dict[str, StackParameter]):
110112
self.resolved_parameters = resolved_parameters
111113
if resolved_parameters:
112114
self.metadata["Parameters"] = list(resolved_parameters.values())
@@ -138,7 +140,7 @@ def describe_details(self):
138140
result["Outputs"] = outputs
139141
stack_parameters = convert_stack_parameters_to_list(self.resolved_parameters)
140142
if stack_parameters:
141-
result["Parameters"] = stack_parameters
143+
result["Parameters"] = [strip_parameter_type(sp) for sp in stack_parameters]
142144
if not result.get("DriftInformation"):
143145
result["DriftInformation"] = {"StackDriftStatus": "NOT_CHECKED"}
144146
for attr in ["Tags", "NotificationARNs"]:

localstack/services/cloudformation/engine/parameters.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,17 @@ def extract_stack_parameter_declarations(template: dict) -> dict[str, ParameterD
4444
return result
4545

4646

47+
class StackParameter(Parameter):
48+
# we need the type information downstream when actually using the resolved value
49+
# e.g. in case of lists so that we know that we should interpret the string as a comma-separated list.
50+
ParameterType: str
51+
52+
4753
def resolve_parameters(
4854
parameter_declarations: dict[str, ParameterDeclaration],
4955
new_parameters: dict[str, Parameter],
5056
old_parameters: dict[str, Parameter],
51-
) -> dict[str, Parameter]:
57+
) -> dict[str, StackParameter]:
5258
"""
5359
Resolves stack parameters or raises an exception if any parameter can not be resolved.
5460
@@ -67,7 +73,7 @@ def resolve_parameters(
6773
# populate values for every parameter declared in the template
6874
for pm in parameter_declarations.values():
6975
pm_key = pm["ParameterKey"]
70-
resolved_param = Parameter(ParameterKey=pm_key)
76+
resolved_param = StackParameter(ParameterKey=pm_key, ParameterType=pm["ParameterType"])
7177
new_parameter = new_parameters.get(pm_key)
7278
old_parameter = old_parameters.get(pm_key)
7379

@@ -126,7 +132,15 @@ def resolve_ssm_parameter(stack_parameter_value: str) -> str:
126132
return connect_to().ssm.get_parameter(Name=stack_parameter_value)["Parameter"]["Value"]
127133

128134

129-
def convert_stack_parameters_to_list(in_params: dict[str, Parameter] | None) -> list[Parameter]:
135+
def strip_parameter_type(in_param: StackParameter) -> Parameter:
136+
result = in_param.copy()
137+
result.pop("ParameterType", None)
138+
return result
139+
140+
141+
def convert_stack_parameters_to_list(
142+
in_params: dict[str, StackParameter] | None
143+
) -> list[StackParameter]:
130144
if not in_params:
131145
return []
132146
return list(in_params.values())
@@ -151,19 +165,19 @@ class LegacyParameter(TypedDict):
151165
Properties: LegacyParameterProperties
152166

153167

154-
def map_to_legacy_structure(parameter_type: str, new_parameter: Parameter) -> LegacyParameter:
168+
# TODO: not actually parameter_type but the logical "ID"
169+
def map_to_legacy_structure(parameter_name: str, new_parameter: StackParameter) -> LegacyParameter:
155170
"""
156171
Helper util to convert a normal (resolved) stack parameter to a legacy parameter structure that can then be merged with stack resources.
157172
158-
:param parameter_type: the stack parameter type (e.g. "String", "AWS::SSM::Parameter::Value<String>", ...)
159173
:param new_parameter: a resolved stack parameter
160174
:return: legacy parameter that can be merged with stack resources for uniform lookup based on logical ID
161175
"""
162176
return LegacyParameter(
163177
LogicalResourceId=new_parameter["ParameterKey"],
164178
Type="Parameter",
165179
Properties=LegacyParameterProperties(
166-
ParameterType=parameter_type,
180+
ParameterType=new_parameter.get("ParameterType"),
167181
ParameterValue=new_parameter.get("ParameterValue"),
168182
ResolvedValue=new_parameter.get("ResolvedValue"),
169183
Value=new_parameter.get("ResolvedValue", new_parameter.get("ParameterValue")),

localstack/services/cloudformation/engine/template_deployer.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,20 @@ def resolve_ref(stack_name: str, resources: dict, mappings: dict, ref: str, attr
261261
raise Exception("Should be detected earlier")
262262

263263
# TODO: remove after refactoring parameter resolution
264+
# TODO: split this apart in parameter resource types and stack parameter handling
264265
if resource["Type"] == "Parameter":
265-
return resource["Properties"]["Value"]
266+
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
267+
# TODO: extract this into a util function and extend type support
268+
parameter_type = resource.get("Properties", {}).get("ParameterType")
269+
if not parameter_type:
270+
# assuming this is an actual resource type now
271+
return resource["Properties"]["Value"]
272+
else:
273+
parameter_type: str = resource["Properties"]["ParameterType"]
274+
if parameter_type in ["CommaDelimitedList"] or parameter_type.startswith("List<"):
275+
return [p.strip() for p in resource["Properties"]["Value"].split(",")]
276+
else:
277+
return resource["Properties"]["Value"]
266278
else:
267279
# TODO: this shouldn't be needed when dependency graph and deployment status is honored
268280
resolve_refs_recursively(stack_name, resources, mappings, resources.get(ref))
@@ -428,9 +440,15 @@ def _resolve_refs_recursively(
428440

429441
if stripped_fn_lower == "join":
430442
join_values = value[keys_list[0]][1]
443+
444+
# this can actually be another ref that produces a list as output
445+
if isinstance(join_values, dict):
446+
join_values = resolve_refs_recursively(stack_name, resources, mappings, join_values)
447+
431448
join_values = [
432449
resolve_refs_recursively(stack_name, resources, mappings, v) for v in join_values
433450
]
451+
434452
none_values = [v for v in join_values if v is None]
435453
if none_values:
436454
raise Exception(

localstack/services/cloudformation/provider.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
StackInstance,
8181
StackSet,
8282
)
83+
from localstack.services.cloudformation.engine.parameters import strip_parameter_type
8384
from localstack.services.cloudformation.engine.template_deployer import NoStackUpdates
8485
from localstack.services.cloudformation.engine.template_preparer import (
8586
FailedTransformationException,
@@ -190,9 +191,7 @@ def create_stack(self, context: RequestContext, request: CreateStackInput) -> Cr
190191
)
191192

192193
# resolve stack parameters
193-
new_parameters: dict[str, Parameter] = param_resolver.convert_stack_parameters_to_dict(
194-
request.get("Parameters")
195-
)
194+
new_parameters = param_resolver.convert_stack_parameters_to_dict(request.get("Parameters"))
196195
parameter_declarations = param_resolver.extract_stack_parameter_declarations(template)
197196
resolved_parameters = param_resolver.resolve_parameters(
198197
parameter_declarations=parameter_declarations,
@@ -490,7 +489,9 @@ def create_change_set(
490489
raise ValidationError(
491490
f"Stack '{stack_name}' does not exist."
492491
) # stack should exist already
493-
old_parameters = stack.resolved_parameters
492+
old_parameters = {
493+
k: strip_parameter_type(v) for k, v in stack.resolved_parameters.items()
494+
}
494495
elif change_set_type == "CREATE":
495496
# create new (empty) stack
496497
if stack is not None:
@@ -612,7 +613,8 @@ def describe_change_set(
612613
"Transform",
613614
]
614615
result = remove_attributes(deepcopy(change_set.metadata), attrs)
615-
# result["Parameters"] = list(change_set.resolved_parameters.values())
616+
# TODO: replace this patch with a better solution
617+
result["Parameters"] = [strip_parameter_type(p) for p in result.get("Parameters", [])]
616618
return result
< C code>617619

618620
@handler("DeleteChangeSet")

tests/integration/cloudformation/api/test_stacks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,27 @@ def test_events_resource_types(deploy_cfn_template, snapshot, aws_client):
589589
resource_types = list(set([event["ResourceType"] for event in events]))
590590
resource_types.sort()
591591
snapshot.match("resource_types", resource_types)
592+
593+
594+
# TODO: rewrite this for proper compatibility with AWS by using a different resource (that doesn't need a deployed VPC)
595+
# technically this is validated, but you'll need to replace the two parameters SubnetParam and SecurityGroupId with valid values
596+
# @pytest.mark.aws_validated
597+
@pytest.mark.only_localstack
598+
def test_list_parameter_type(aws_client, deploy_cfn_template, cleanups, lambda_su_role):
599+
stack_name = f"test-stack-{short_uid()}"
600+
cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_name))
601+
deploy_cfn_template(
602+
template_path=os.path.join(
603+
os.path.dirname(__file__), "../../templates/cfn_parameter_list_type.yaml"
604+
),
605+
parameters={
606+
"SubnetParam": "subnet-1,subnet-2",
607+
"SecurityGroupId": "sg-1",
608+
"FunctionRole": lambda_su_role,
609+
},
610+
)
611+
612+
# TODO: the lambda provider doesn't currently support the vpc config (even as CRUD-only)
613+
# vpc_config = aws_client.awslambda.get_function_configuration(FunctionName=stack.outputs["LambdaName"])['VpcConfig']
614+
# assert vpc_config['SecurityGroupIds'] == ["sg-1"]
615+
# assert vpc_config['SubnetIds'] == ["subnet-1", "subnet-2"]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Parameters:
2+
SubnetParam:
3+
# fun fact: this works but is not documented in the docs
4+
# see: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
5+
Type: List<String>
6+
SecurityGroupId:
7+
Type: String
8+
FunctionRole:
9+
Type: String
10+
Resources:
11+
Function76856677:
12+
Type: AWS::Lambda::Function
13+
Properties:
14+
Code:
15+
ZipFile: |
16+
def handler(event, context):
17+
return {"hello": "world"}
18+
Role: !Ref FunctionRole
19+
Handler: index.handler
20+
Runtime: python3.9
21+
VpcConfig:
22+
SecurityGroupIds:
23+
- !Ref SecurityGroupId
24+
SubnetIds:
25+
Ref: SubnetParam
26+
Outputs:
27+
LambdaName:
28+
Value:
29+
Ref: Function76856677

0 commit comments

Comments
 (0)
0