"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import os
from cfnlint.decode import decode
from cfnlint.rules import CloudFormationLintRule, RuleMatch
class NestedStackParameters(CloudFormationLintRule):
"""Check that nested stack parameters are specified as needed"""
id = "E3043"
shortdesc = "Validate parameters for in a nested stack"
description = (
"Evalute if parameters for a nested stack are specified and "
"if parameters are specified for a nested stack that aren't required."
)
source_url = "https://github.com/awslabs/cfn-python-lint"
tags = ["resources", "cloudformation"]
def __init__(self):
"""Init"""
super().__init__()
self.resource_property_types.append("AWS::CloudFormation::Stack")
def __get_template_parameters(self, filename):
try:
(tmp, matches) = decode(filename)
except: # pylint: disable=bare-except
return None
if matches:
return None
return tmp.get("Parameters", {})
def __compare_objects(self, template_parameters, nested_parameters, scenario, path):
matches = []
for key in set(template_parameters.keys()) - set(nested_parameters.keys()):
if scenario is None:
message = 'Specified parameter "{0}" doesn\'t exist in nested stack template at {1}'
matches.append(
RuleMatch(
path + [key],
message.format(key, "/".join(map(str, path + [key]))),
)
)
else:
message = 'Specified parameter "{0}" doesn\'t exist in nested stack template {1}'
scenario_text = " and ".join(
[f'when condition "{k}" is {v}' for (k, v) in scenario.items()]
)
matches.append(RuleMatch(path, message.format(key, scenario_text)))
for key in set(nested_parameters.keys()) - set(template_parameters.keys()):
if nested_parameters.get(key).get("Default") is None:
if scenario is None:
message = (
'Nested stack template parameter "{0}" is not specified at {1}'
)
matches.append(
RuleMatch(path, message.format(key, "/".join(map(str, path))))
)
else:
message = (
'Nested stack template parameter "{0}" is not specified {1}'
)
scenario_text = " and ".join(
[f'when condition "{k}" is {v}' for (k, v) in scenario.items()]
)
matches.append(RuleMatch(path, message.format(key, scenario_text)))
return matches
def match_resource_properties(self, properties, _, path, cfn):
"""Check CloudFormation Properties"""
matches = []
# when template is passed via cat (or equivalent filename is none)
if cfn.filename:
base_dir = os.path.dirname(os.path.abspath(cfn.filename))
parameter_groups = cfn.get_object_without_conditions(
obj=properties, property_names=["TemplateURL", "Parameters"]
)
for parameter_group in parameter_groups:
obj = parameter_group.get("Object")
template_url = obj.get("TemplateURL")
if isinstance(template_url, str):
if not (
template_url.startswith("http://")
or template_url.startswith("https://")
or template_url.startswith("s3://")
):
template_path = os.path.normpath(
os.path.join(base_dir, template_url)
)
nested_parameters = self.__get_template_parameters(
template_path
)
template_parameters = obj.get("Parameters")
if isinstance(nested_parameters, dict) and isinstance(
template_parameters, dict
):
matches.extend(
self.__compare_objects(
template_parameters=template_parameters,
nested_parameters=nested_parameters,
path=path + ["Parameters"],
scenario=parameter_group.get("Scenario"),
)
)
return matches