Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions strong_opx/hcl/runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import subprocess
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from strong_opx.exceptions import UndefinedVariableError
from strong_opx.hcl.extractor import HCLVariableExtractor
Expand All @@ -18,6 +18,51 @@ def __init__(self, environment: "Environment", directory: str):
self.environment = environment
self.directory = directory

@staticmethod
def _serialize_value(value: Any) -> str:
"""
Converts a variable value to a string that can be used as an environment variable that will be passed to a
Terraform or Packer command.

Per Terraform documentation:
https://developer.hashicorp.com/terraform/cli/commands/plan#input-variables-on-the-command-line

string, number, and bool values are expected to be passed as strings with no special punctuation. For all other
type constraints, including list, map, and set types and the special any keyword, you must write a valid
Terraform language expression representing the value, and write any necessary quoting or escape characters to
ensure it will pass through your shell literally to Terraform.

Packer documentation does not explicitly call this out, but, in testing, this code works for Packer as well.

NOTE: This method does NOT handle Windows command line escaping, so it should only be used in Unix environments.
strong-opx does not currently support Windows, but I wanted to explicitly call it out here.
"""
if isinstance(value, list) or isinstance(value, tuple):
build_string = "["
for item in value:
if isinstance(item, str):
build_string += f'"{item}",'
else:
build_string += f"{HCLRunner._serialize_value(item)},"
if build_string.endswith(","):
build_string = build_string[:-1] # remove trailing comma
build_string += "]"
return build_string
elif isinstance(value, dict):
build_string = "{"
for key, val in value.items():
if isinstance(val, str):
build_string += f'"{key}": "{val}",'
else:
build_string += f'"{key}": {HCLRunner._serialize_value(val)},'
if build_string.endswith(","):
build_string = build_string[:-1] # remove trailing comma

build_string += "}"

return build_string
return str(value)

def extract_vars(self) -> dict[str, str]:
env_dict = {}
extractor = HCLVariableExtractor()
Expand All @@ -36,14 +81,14 @@ def extract_vars(self) -> dict[str, str]:
missing_vars.append(var)
continue

env_dict[f"{self.env_var_prefix}_{var}"] = str(context[var])
env_dict[f"{self.env_var_prefix}_{var}"] = self._serialize_value(context[var])

if missing_vars:
raise UndefinedVariableError(*missing_vars)

for var in extractor.optional_vars:
if var in context:
env_dict[f"{self.env_var_prefix}_{var}"] = str(context[var])
env_dict[f"{self.env_var_prefix}_{var}"] = self._serialize_value(context[var])

return env_dict

Expand Down
31 changes: 27 additions & 4 deletions tests/hcl/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,38 @@ def test_variable_extraction_only_from_whitelisted_files(
@mock.patch("strong_opx.hcl.runner.HCLVariableExtractor")
def test_required_and_optional_vars_with_none_missing(self, extractor_mock: mock.Mock, listdir_mock: mock.Mock):
listdir_mock.return_value = []
extractor_mock.return_value.required_vars = ["VAR_1", "VAR_2"]
extractor_mock.return_value.optional_vars = ["VAR_3"]
extractor_mock.return_value.required_vars = ["VAR_1", "VAR_2", "ARRAY_OF_INTS", "MIXED_ARRAY", "TUPLE"]
extractor_mock.return_value.optional_vars = ["ARRAY_OF_STRINGS", "ARRAY_OF_ARRAYS_OF_STRINGS", "NESTED_MIX"]

runner = TestHCLRunner()
runner.environment.context = Context({"VAR_1": "VAL_1", "VAR_2": "VAL_2", "VAR_3": "VAL_3"})
runner.environment.context = Context(
{
"VAR_1": "VAL_1",
"VAR_2": 2,
"ARRAY_OF_STRINGS": ["VAL_3_1", "VAL_3_2"],
"ARRAY_OF_INTS": [1, 2, 3],
"ARRAY_OF_ARRAYS_OF_STRINGS": [["VAL_4_1_1", "VAL_4_1_2"], ["VAL_4_2_1", "VAL_4_2_2"]],
"MIXED_ARRAY": ["VAL_5_1", 2, ["VAL_5_3_1", "VAL_5_3_2"]],
"NESTED_MIX": {"KEY1": "VAL_6_1", "KEY2": ["VAL_6_2_1", "VAL_6_2_2"]},
"TUPLE": ("TUPLE_1", "TUPLE_2"),
}
)

actual_result = runner.extract_vars()

self.assertDictEqual({"T_VAR_1": "VAL_1", "T_VAR_2": "VAL_2", "T_VAR_3": "VAL_3"}, actual_result)
self.assertDictEqual(
{
"T_VAR_1": "VAL_1",
"T_VAR_2": "2",
"T_ARRAY_OF_STRINGS": '["VAL_3_1","VAL_3_2"]',
"T_ARRAY_OF_INTS": "[1,2,3]",
"T_ARRAY_OF_ARRAYS_OF_STRINGS": '[["VAL_4_1_1","VAL_4_1_2"],["VAL_4_2_1","VAL_4_2_2"]]',
"T_MIXED_ARRAY": '["VAL_5_1",2,["VAL_5_3_1","VAL_5_3_2"]]',
"T_NESTED_MIX": '{"KEY1": "VAL_6_1","KEY2": ["VAL_6_2_1","VAL_6_2_2"]}',
"T_TUPLE": '["TUPLE_1","TUPLE_2"]',
},
actual_result,
)

@mock.patch("strong_opx.hcl.runner.os.listdir")
@mock.patch("strong_opx.hcl.runner.HCLVariableExtractor")
Expand Down
Loading