• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 20357994067

18 Dec 2025 10:01PM UTC coverage: 86.913% (-0.02%) from 86.929%
20357994067

push

github

web-flow
Fix CloudWatch model annotation (#13545)

1 of 1 new or added line in 1 file covered. (100.0%)

1391 existing lines in 33 files now uncovered.

70000 of 80540 relevant lines covered (86.91%)

1.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.11
/localstack-core/localstack/services/cloudformation/engine/parameters.py
1
"""
2
TODO: ordering & grouping of parameters
3
TODO: design proper structure for parameters to facilitate validation etc.
4
TODO: clearer language around both parameters and "resolving"
5

6
Documentation extracted from AWS docs (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html):
7
    The following requirements apply when using parameters:
8

9
        You can have a maximum of 200 parameters in an AWS CloudFormation template.
10
        Each parameter must be given a logical name (also called logical ID), which must be alphanumeric and unique among all logical names within the template.
11
        Each parameter must be assigned a parameter type that is supported by AWS CloudFormation. For more information, see Type.
12
        Each parameter must be assigned a value at runtime for AWS CloudFormation to successfully provision the stack. You can optionally specify a default value for AWS CloudFormation to use unless another value is provided.
13
        Parameters must be declared and referenced from within the same template. You can reference parameters from the Resources and Outputs sections of the template.
14

15
        When you create or update stacks and create change sets, AWS CloudFormation uses whatever values exist in Parameter Store at the time the operation is run. If a specified parameter doesn't exist in Parameter Store under the caller's AWS account, AWS CloudFormation returns a validation error.
16

17
        For stack updates, the Use existing value option in the console and the UsePreviousValue attribute for update-stack tell AWS CloudFormation to use the existing Systems Manager parameter key—not its value. AWS CloudFormation always fetches the latest values from Parameter Store when it updates stacks.
18

19
"""
20

21
import logging
2✔
22
from typing import Literal, TypedDict
2✔
23

24
from botocore.exceptions import ClientError
2✔
25

26
from localstack.aws.api.cloudformation import Parameter, ParameterDeclaration
2✔
27
from localstack.aws.connect import connect_to
2✔
28

29
LOG = logging.getLogger(__name__)
2✔
30

31

32
def extract_stack_parameter_declarations(template: dict) -> dict[str, ParameterDeclaration]:
2✔
33
    """
34
    Extract and build a dict of stack parameter declarations from a CloudFormation stack templatef
35

36
    :param template: the parsed CloudFormation stack template
37
    :return: a dictionary of declared parameters, mapping logical IDs to the corresponding parameter declaration
38
    """
UNCOV
39
    result = {}
1✔
UNCOV
40
    for param_key, param in template.get("Parameters", {}).items():
1✔
UNCOV
41
        result[param_key] = ParameterDeclaration(
1✔
42
            ParameterKey=param_key,
43
            DefaultValue=param.get("Default"),
44
            ParameterType=param.get("Type"),
45
            NoEcho=param.get("NoEcho", False),
46
            # TODO: test & implement rest here
47
            # ParameterConstraints=?,
48
            # Description=?
49
        )
UNCOV
50
    return result
1✔
51

52

53
class StackParameter(Parameter):
2✔
54
    # we need the type information downstream when actually using the resolved value
55
    # e.g. in case of lists so that we know that we should interpret the string as a comma-separated list.
56
    ParameterType: str
2✔
57

58

59
def resolve_parameters(
2✔
60
    account_id: str,
61
    region_name: str,
62
    parameter_declarations: dict[str, ParameterDeclaration],
63
    new_parameters: dict[str, Parameter],
64
    old_parameters: dict[str, Parameter],
65
) -> dict[str, StackParameter]:
66
    """
67
    Resolves stack parameters or raises an exception if any parameter can not be resolved.
68

69
    Assumptions:
70
        - There are no extra undeclared parameters given (validate before calling this method)
71

72
    TODO: is UsePreviousValue=False equivalent to not specifying it, in all situations?
73

74
    :param parameter_declarations: The parameter declaration from the (potentially new) template, i.e. the "Parameters" section
75
    :param new_parameters: The parameters to resolve
76
    :param old_parameters: The old parameters from the previous stack deployment, if available
77
    :return: a copy of new_parameters with resolved values
78
    """
UNCOV
79
    resolved_parameters = {}
1✔
80

81
    # populate values for every parameter declared in the template
UNCOV
82
    for pm in parameter_declarations.values():
1✔
UNCOV
83
        pm_key = pm["ParameterKey"]
1✔
UNCOV
84
        resolved_param = StackParameter(ParameterKey=pm_key, ParameterType=pm["ParameterType"])
1✔
UNCOV
85
        new_parameter = new_parameters.get(pm_key)
1✔
UNCOV
86
        old_parameter = old_parameters.get(pm_key)
1✔
87

UNCOV
88
        if new_parameter is None:
1✔
89
            # since no value has been specified for the deployment, we need to be able to resolve the default or fail
UNCOV
90
            default_value = pm["DefaultValue"]
1✔
UNCOV
91
            if default_value is None:
1✔
92
                LOG.error("New parameter without a default value: %s", pm_key)
×
93
                raise Exception(
×
94
                    f"Invalid. Parameter '{pm_key}' needs to have either param specified or Default."
95
                )  # TODO: test and verify
96

UNCOV
97
            resolved_param["ParameterValue"] = default_value
1✔
98
        else:
UNCOV
99
            if (
1✔
100
                new_parameter.get("UsePreviousValue", False)
101
                and new_parameter.get("ParameterValue") is not None
102
            ):
103
                raise Exception(
×
104
                    f"Can't set both 'UsePreviousValue' and a concrete value for parameter '{pm_key}'."
105
                )  # TODO: test and verify
106

UNCOV
107
            if new_parameter.get("UsePreviousValue", False):
1✔
UNCOV
108
                if old_parameter is None:
1✔
109
                    raise Exception(
×
110
                        f"Set 'UsePreviousValue' but stack has no previous value for parameter '{pm_key}'."
111
                    )  # TODO: test and verify
112

UNCOV
113
                resolved_param["ParameterValue"] = old_parameter["ParameterValue"]
1✔
114
            else:
UNCOV
115
                resolved_param["ParameterValue"] = new_parameter["ParameterValue"]
1✔
116

UNCOV
117
        resolved_param["NoEcho"] = pm.get("NoEcho", False)
1✔
UNCOV
118
        resolved_parameters[pm_key] = resolved_param
1✔
119

120
        # Note that SSM parameters always need to be resolved anew here
121
        # TODO: support more parameter types
UNCOV
122
        if pm["ParameterType"].startswith("AWS::SSM"):
1✔
UNCOV
123
            if pm["ParameterType"] in [
1✔
124
                "AWS::SSM::Parameter::Value<String>",
125
                "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
126
                "AWS::SSM::Parameter::Value<CommaDelimitedList>",
127
            ]:
128
                # TODO: error handling (e.g. no permission to lookup SSM parameter or SSM parameter doesn't exist)
UNCOV
129
                resolved_param["ResolvedValue"] = resolve_ssm_parameter(
1✔
130
                    account_id, region_name, resolved_param["ParameterValue"]
131
                )
132
            else:
133
                raise Exception(f"Unsupported stack parameter type: {pm['ParameterType']}")
×
134

UNCOV
135
    return resolved_parameters
1✔
136

137

138
# TODO: inject credentials / client factory for proper account/region lookup
139
def resolve_ssm_parameter(account_id: str, region_name: str, stack_parameter_value: str) -> str:
2✔
140
    """
141
    Resolve the SSM stack parameter from the SSM service with a name equal to the stack parameter value.
142
    """
143
    ssm_client = connect_to(aws_access_key_id=account_id, region_name=region_name).ssm
2✔
144
    try:
2✔
145
        return ssm_client.get_parameter(Name=stack_parameter_value)["Parameter"]["Value"]
2✔
146
    except ClientError:
2✔
147
        LOG.error("client error fetching parameter '%s'", stack_parameter_value)
2✔
148
        raise
2✔
149

150

151
def strip_parameter_type(in_param: StackParameter) -> Parameter:
2✔
UNCOV
152
    result = in_param.copy()
1✔
UNCOV
153
    result.pop("ParameterType", None)
1✔
UNCOV
154
    return result
1✔
155

156

157
def mask_no_echo(in_param: StackParameter) -> Parameter:
2✔
UNCOV
158
    result = in_param.copy()
1✔
UNCOV
159
    no_echo = result.pop("NoEcho", False)
1✔
UNCOV
160
    if no_echo:
1✔
UNCOV
161
        result["ParameterValue"] = "****"
1✔
UNCOV
162
    return result
1✔
163

164

165
def convert_stack_parameters_to_list(
2✔
166
    in_params: dict[str, StackParameter] | None,
167
) -> list[StackParameter]:
UNCOV
168
    if not in_params:
1✔
UNCOV
169
        return []
1✔
UNCOV
170
    return list(in_params.values())
1✔
171

172

173
def convert_stack_parameters_to_dict(in_params: list[Parameter] | None) -> dict[str, Parameter]:
2✔
UNCOV
174
    if not in_params:
1✔
UNCOV
175
        return {}
1✔
UNCOV
176
    return {p["ParameterKey"]: p for p in in_params}
1✔
177

178

179
class LegacyParameterProperties(TypedDict):
2✔
180
    Value: str
2✔
181
    ParameterType: str
2✔
182
    ParameterValue: str | None
2✔
183
    ResolvedValue: str | None
2✔
184

185

186
class LegacyParameter(TypedDict):
2✔
187
    LogicalResourceId: str
2✔
188
    Type: Literal["Parameter"]
2✔
189
    Properties: LegacyParameterProperties
2✔
190

191

192
# TODO: not actually parameter_type but the logical "ID"
193
def map_to_legacy_structure(parameter_name: str, new_parameter: StackParameter) -> LegacyParameter:
2✔
194
    """
195
    Helper util to convert a normal (resolved) stack parameter to a legacy parameter structure that can then be merged with stack resources.
196

197
    :param new_parameter: a resolved stack parameter
198
    :return: legacy parameter that can be merged with stack resources for uniform lookup based on logical ID
199
    """
200
    return LegacyParameter(
×
201
        LogicalResourceId=new_parameter["ParameterKey"],
202
        Type="Parameter",
203
        Properties=LegacyParameterProperties(
204
            ParameterType=new_parameter.get("ParameterType"),
205
            ParameterValue=new_parameter.get("ParameterValue"),
206
            ResolvedValue=new_parameter.get("ResolvedValue"),
207
            Value=new_parameter.get("ResolvedValue", new_parameter.get("ParameterValue")),
208
        ),
209
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc