diff --git a/requirements.txt b/requirements.txt index 32b07ed..52f182a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ pyyaml>=6.0 twine>=3.8 build>=0.7 flake8==7.1.1 -pylint==3.3.1 \ No newline at end of file +pylint==3.3.1 +timeout-decorator==0.5.0 \ No newline at end of file diff --git a/src/pycomposefile/compose_element/compose_datatype_transformer.py b/src/pycomposefile/compose_element/compose_datatype_transformer.py index d0915e0..2baf4ac 100644 --- a/src/pycomposefile/compose_element/compose_datatype_transformer.py +++ b/src/pycomposefile/compose_element/compose_datatype_transformer.py @@ -59,7 +59,7 @@ def update_value_with_resolved_environment(self, env_variable_name, env_variable return re.sub(env_variable_name, env_variable_value, source_string) def replace_environment_variables_with_empty_unset(self, value): - capture = re.compile(r"(\$+)\{(?P\w+)\:-(?P.+)\}") + capture = re.compile(r"(\$+)\{(?P\w+)\:-(?P[^$]*?)\}") matches = capture.search(value) if matches is not None and matches[1] == "$$": return value @@ -68,7 +68,8 @@ def replace_environment_variables_with_empty_unset(self, value): default_value = matches.group("defaultvalue") if env_var is None or len(env_var) == 0: env_var = default_value - value = self.update_value_with_resolved_environment(f"\\{matches[0]}", env_var, value) + escaped = re.sub(r"([*+?{}])", r"\\\1", matches[0]) # escape special characters + value = self.update_value_with_resolved_environment(f"\\{escaped}", env_var, value) matches = capture.search(value) return value diff --git a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py index 50334d5..99c018e 100644 --- a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py +++ b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py @@ -2,7 +2,7 @@ import os from ..compose_generator import ComposeGenerator from pycomposefile.compose_element import EmptyOrUnsetException - +import timeout_decorator class TestBracesNoUnderscoreNoDigitVariableInterpolation(TestCase): @@ -19,6 +19,21 @@ def test_uppercase_with_default_when_unset_in_string_value(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + def test_uppercase_without_default_when_unset_in_string_value(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/") + + @timeout_decorator.timeout(5) + def test_uppercase_with_nested_default_when_unset_in_string_value(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-${CHILDUNSET:-bob}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + @mock.patch.dict(os.environ, {"TESTCPUCOUNT": "1.5"}) def test_uppercase_in_decimal_value(self): braced_env_var = "{TESTCPUCOUNT}" @@ -48,6 +63,21 @@ def test_lowercase_with_default_when_unset_in_string_value(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + def test_lowercase_without_default_when_unset_in_string_value(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/") + + @timeout_decorator.timeout(5) + def test_lowercase_with_nested_default_when_unset_in_string_value(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-${childunset:-bob}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + @mock.patch.dict(os.environ, {"testcpucount": "1.5"}) def test_lowercase_in_decimal_value(self): braced_env_var = "{testcpucount}" @@ -88,3 +118,86 @@ def test_service_with_double_dollar_sign_env_vars(self): compose_file = ComposeGenerator.get_compose_with_double_dollar_sign_env_vars() self.assertEqual(compose_file.services["frontend"].environment["ENVIRONMENT"], "$ENVIRONMENT") + + @timeout_decorator.timeout(5) + def test_uppercase_with_asterisk_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-*}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/*") + + @timeout_decorator.timeout(5) + def test_uppercase_with_plus_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-+}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/+") + + @timeout_decorator.timeout(5) + def test_uppercase_with_question_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-?}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + + @timeout_decorator.timeout(5) + def test_uppercase_with_rightbrace_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-a}-}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/a-}") # + + @timeout_decorator.timeout(5) + def test_lowercase_with_asterisk_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-*}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/*") + + @timeout_decorator.timeout(5) + def test_lowercase_with_plus_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-+}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/+") + + @timeout_decorator.timeout(5) + def test_lowercase_with_question_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-?}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + + @timeout_decorator.timeout(5) + def test_lowercase_with_rightbrace_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/}") + + @timeout_decorator.timeout(5) + def test_uppercase_two_variables_with_default_in_string_value(self): + braced_first_env_var = "{HOSTPORT:-8080}" + braced_second_env_var = "{CONTAINERPORT:-80}" + compose_file = ComposeGenerator.get_with_two_environment_variables_in_string_value(braced_first_env_var, braced_second_env_var) + self.assertEqual(f"{compose_file.services['frontend'].ports[0]}", "8080:80/tcp") + self.assertEqual(compose_file.services['frontend'].ports[0].published, "8080") + self.assertEqual(compose_file.services['frontend'].ports[0].target, "80") + + @timeout_decorator.timeout(5) + def test_lowercase_two_variables_with_default_in_string_value(self): + braced_first_env_var = "{httpport:-8080}" + braced_second_env_var = "{containerport:-80}" + compose_file = ComposeGenerator.get_with_two_environment_variables_in_string_value(braced_first_env_var, braced_second_env_var) + self.assertEqual(f"{compose_file.services['frontend'].ports[0]}", "8080:80/tcp") + self.assertEqual(compose_file.services['frontend'].ports[0].published, "8080") + self.assertEqual(compose_file.services['frontend'].ports[0].target, "80") +