Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added module output resolving #4259

Merged
merged 8 commits into from
Sep 30, 2022
Merged
109 changes: 103 additions & 6 deletions samcli/hook_packages/terraform/hooks/prepare/resource_linking.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class References:
@dataclass
class ResolvedReference:
value: str
module_address: str
module_address: Optional[str]


@dataclass
Expand Down Expand Up @@ -107,20 +107,115 @@ def _get_configuration_address(address: str) -> str:
Cleans all addresses of indices and returns a clean address

Parameters
==========
----------
address : str
The address to clean

Returns
=======
-------
str
The address clean of indices
"""
return re.sub(r"\[[^\[\]]*\]", "", address)


def _resolve_module_output(module, output_name):
pass
def _resolve_module_output(module: TFModule, output_name: str) -> List[Union[ConstantValue, ResolvedReference]]:
"""
Resolves any references in the output section of the module

Parameters
----------
module : Module
The module with outputs to search
output_name : str
The value to resolve

Returns
-------
List[Union[ConstantValue, ResolvedReference]]
A list of resolved values
"""
results: List[Union[ConstantValue, ResolvedReference]] = []

output = module.outputs.get(output_name)

if not output:
raise InvalidResourceLinkingException(f"Output {output_name} was not found in module {module.full_address}")

output_value = output.value

LOG.debug("Resolving output {%s} for module {%s}", output_name, module.full_address)

if isinstance(output, ConstantValue):
LOG.debug(
"Resolved constant value {%s} for module {%s} for output {%s}",
output.value,
module.full_address,
output_name,
)

results.append(output)
elif isinstance(output, References):
LOG.debug("Found references for module {%s} for output {%s}", module.full_address, output_name)

cleaned_references = _clean_references_list(output_value)

for reference in cleaned_references:
if reference.startswith("var."):
LOG.debug(
"Resolving variable reference {%s} for module {%s} for output {%s}",
reference,
module.full_address,
output_name,
)

stripped_reference = _get_configuration_address(reference[reference.find(".") + 1 :])
results += _resolve_module_variable(module, stripped_reference)
elif reference.startswith("module."):
LOG.debug(
"Resolving module reference {%s} for module {%s} for output {%s}",
reference,
module.full_address,
output_name,
)

# validate that the reference is in the format: module.name.output
if re.fullmatch(r"module(?:\.[^\.]+){2}", reference) is None:
raise InvalidResourceLinkingException(
f"Module {module.full_address} contains an invalid reference {reference}"
)

# module.bbb.ccc => bbb
module_name = reference[reference.find(".") + 1 : reference.rfind(".")]
# module.bbb.ccc => ccc
output_name = reference[reference.rfind(".") + 1 :]

stripped_reference = _get_configuration_address(module_name)

if not module.child_modules:
raise InvalidResourceLinkingException(
f"Module {module.full_address} does not have child modules defined"
)

child_module = module.child_modules.get(stripped_reference)

if not child_module:
raise InvalidResourceLinkingException(
f"Module {module.full_address} does not have {stripped_reference} as a child module"
)

results += _resolve_module_output(child_module, output_name)
else:
LOG.debug(
"Resolved reference {%s} for module {%s} for output {%s}",
reference,
module.full_address,
output_name,
)

results.append(ResolvedReference(reference, module.full_address))

return results


def _resolve_module_variable(module: TFModule, variable_name: str) -> List[Union[ConstantValue, ResolvedReference]]:
Expand Down Expand Up @@ -163,7 +258,9 @@ def _resolve_module_variable(module: TFModule, variable_name: str) -> List[Union
and module.parent_module.child_modules
and module.parent_module.child_modules.get(config_module_name)
):
child_module = module.parent_module.child_modules.get(config_module_name)
# using .get() gives us Optional[TFModule], if conditional already validates child module exists
# access list directly instead
child_module = module.parent_module.child_modules[config_module_name]
results += _resolve_module_output(child_module, output_name)
else:
raise InvalidResourceLinkingException(f"Couldn't find child module {config_module_name}.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
from unittest.mock import Mock, patch

from parameterized import parameterized
from samcli.hook_packages.terraform.hooks.prepare.exceptions import InvalidResourceLinkingException

from samcli.hook_packages.terraform.hooks.prepare.exceptions import InvalidResourceLinkingException
from samcli.hook_packages.terraform.hooks.prepare.resource_linking import (
ResolvedReference,
_clean_references_list,
_get_configuration_address,
_resolve_module_output,
TFModule,
TFResource,
ConstantValue,
References,
ConstantValue,
_resolve_module_variable,
)

Expand Down Expand Up @@ -134,6 +137,152 @@ def test_resource_full_address_root_module(self):
resource = TFResource("resource_address", "type", module, {})
self.assertEqual(resource.full_address, "resource_address")

@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._resolve_module_variable")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_with_var(self, clean_ref_mock, config_mock, resolve_var_mock):
constant_val = ConstantValue("mycoolvar")

module = TFModule(
None,
None,
{"mycoolref": constant_val},
[],
{},
{"mycooloutput": References(["var.mycoolref"])},
)

config_mock.return_value = "mycoolref"
clean_ref_mock.return_value = ["var.mycoolref"]
resolve_var_mock.return_value = [constant_val]

results = _resolve_module_output(module, "mycooloutput")

# assert we are calling the right funcs
config_mock.assert_called_with("mycoolref")
resolve_var_mock.assert_called_with(module, "mycoolref")

# assert we still return valid results
self.assertEqual(len(results), 1)
self.assertEqual(results[0].value, "mycoolvar")
self.assertIsInstance(results[0], ConstantValue)

@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_with_module(self, clean_ref_mock, config_mock):
module = TFModule(None, None, {}, [], {}, {"mycooloutput": References(["module.mycoolmod.mycooloutput2"])})
module2 = TFModule("module.mycoolmod", module, {}, [], {}, {"mycooloutput2": ConstantValue("mycoolconst")})
module.child_modules.update({"mycoolmod": module2})

config_mock.return_value = "mycoolmod"
clean_ref_mock.return_value = ["module.mycoolmod.mycooloutput2"]

results = _resolve_module_output(module, "mycooloutput")

self.assertEqual(len(results), 1)
self.assertEqual(results[0].value, "mycoolconst")

@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_already_resolved_constant(self, clean_ref_mock, config_mock):
module = TFModule(None, None, {}, [], {}, {"mycooloutput": ConstantValue("mycoolconst")})

results = _resolve_module_output(module, "mycooloutput")

self.assertEqual(len(results), 1)
self.assertEqual(results[0].value, "mycoolconst")
self.assertIsInstance(results[0], ConstantValue)

@parameterized.expand(
[
(
TFModule("module.name", None, {}, [], {}, {"mycooloutput": References(["local.mycoolconst"])}),
"module.name",
),
(TFModule(None, None, {}, [], {}, {"mycooloutput": References(["local.mycoolconst"])}), None),
]
)
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_already_resolved_reference(self, module, expected_addr, clean_ref_mock, config_mock):
clean_ref_mock.return_value = ["local.mycoolconst"]

results = _resolve_module_output(module, "mycooloutput")

self.assertEqual(len(results), 1)
self.assertEqual(results[0].value, "local.mycoolconst")
self.assertEqual(results[0].module_address, expected_addr)
self.assertIsInstance(results[0], ResolvedReference)

@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_raises_exception_empty_output(self, clean_ref_mock, get_config_mock):
module = TFModule("module.mymod", None, {}, [], {}, {})

with self.assertRaises(InvalidResourceLinkingException) as err:
_resolve_module_output(module, "empty")

self.assertEqual(
str(err.exception),
"An error occurred when attempting to link two resources: Output empty was not found in module module.mymod",
)

@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_raises_exception_empty_children(self, clean_ref_mock, get_config_mock):
module = TFModule("module.mymod", None, {}, [], {}, {"search": References(["module.nonexist.output"])})

clean_ref_mock.return_value = ["module.nonexist.output"]
get_config_mock.return_value = "nonexist"

with self.assertRaises(InvalidResourceLinkingException) as err:
_resolve_module_output(module, "search")

self.assertEqual(
str(err.exception),
"An error occurred when attempting to link two resources: Module module.mymod does not have child modules defined",
)

@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_raises_exception_non_exist_child(self, clean_ref_mock, get_config_mock):
module = TFModule(
"module.mymod", None, {}, [], {"othermod": Mock()}, {"search": References(["module.nonexist.output"])}
)
clean_ref_mock.return_value = ["module.nonexist.output"]
get_config_mock.return_value = "nonexist"

with self.assertRaises(InvalidResourceLinkingException) as err:
_resolve_module_output(module, "search")

self.assertEqual(
str(err.exception),
"An error occurred when attempting to link two resources: Module module.mymod does not have nonexist as a child module",
)

@parameterized.expand(
[
"module.",
"module..",
"module.....",
"module.name",
"module.name.output.again",
]
)
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._get_configuration_address")
@patch("samcli.hook_packages.terraform.hooks.prepare.resource_linking._clean_references_list")
def test_resolve_module_output_invalid_module_name(self, invalid_reference, clean_ref_mock, get_config_mock):
module = TFModule("module.name", None, {}, [], {}, {"output1": References([invalid_reference])})
clean_ref_mock.return_value = [invalid_reference]

with self.assertRaises(InvalidResourceLinkingException) as err:
_resolve_module_output(module, "output1")

self.assertEqual(
str(err.exception),
f"An error occurred when attempting to link two resources: Module module.name contains an invalid reference {invalid_reference}",
)

def test_resolve_module_variable_constant_value(self):
constant_value = ConstantValue(value="layer.arn")
module = TFModule(
Expand Down