From 64185f28bc572b9918d1dfc885e3487d103787a2 Mon Sep 17 00:00:00 2001 From: Eli Atzaba Date: Sat, 29 Apr 2017 03:13:44 +0300 Subject: [PATCH] [Fix 4493] Support for Required Environment Variables Signed-off-by: Eli Atzaba --- compose/config/config.py | 7 +++++++ compose/config/interpolation.py | 22 ++++++++++++++++------ tests/unit/config/interpolation_test.py | 9 +++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index f1195c8ec67..59b1eade7b0 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -1019,6 +1019,13 @@ def parse_ulimits(ulimits): def resolve_env_var(key, val, environment): if val is not None: return key, val + elif environment and key.endswith('?'): + required_key = key[:-1] + if required_key not in environment: + raise ConfigurationError( + 'environment variable "{key}" is required'.format(key=key)) + else: + return required_key, environment[required_key] elif environment and key in environment: return key, environment[key] else: diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 1b270b9eab6..81afbb43c4f 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -23,8 +23,8 @@ def __init__(self, templater, mapping): def interpolate(self, string): try: return self.templater(string).substitute(self.mapping) - except ValueError: - raise InvalidInterpolation(string) + except ValueError as e: + raise InvalidInterpolation(string, e) def interpolate_environment_variables(version, config, section, environment): @@ -51,11 +51,12 @@ def interpolate_value(name, config_key, value, section, interpolator): except InvalidInterpolation as e: raise ConfigurationError( 'Invalid interpolation format for "{config_key}" option ' - 'in {section} "{name}": "{string}"'.format( + 'in {section} "{name}": "{string}" caused by {value_error}'.format( config_key=config_key, name=name, section=section, - string=e.string)) + string=e.string, + value_error=e.value_error)) def recursive_interpolate(obj, interpolator): @@ -72,15 +73,23 @@ def recursive_interpolate(obj, interpolator): class TemplateWithDefaults(Template): - idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?' + idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?\??' # Modified from python2.7/string.py def substitute(self, mapping): + # Helper function for .sub() def convert(mo): # Check the most common path first. named = mo.group('named') or mo.group('braced') if named is not None: + if named.endswith('?'): + var = named[:-1] + value = mapping.get(var) + if not value: + raise ValueError('required environment variable "{var}" not set' + .format(var=var)) + return value if ':-' in named: var, _, default = named.partition(':-') return mapping.get(var) or default @@ -99,5 +108,6 @@ def convert(mo): class InvalidInterpolation(Exception): - def __init__(self, string): + def __init__(self, string, value_error): self.string = string + self.value_error = value_error diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index 256c74d9bed..f2b0dd47f35 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -144,3 +144,12 @@ def test_interpolate_missing_with_default(defaults_interpolator): def test_interpolate_with_empty_and_default_value(defaults_interpolator): assert defaults_interpolator("ok ${BAR:-def}") == "ok def" assert defaults_interpolator("ok ${BAR-def}") == "ok " + + +def test_interpolate_with_missing_required_environment_variable(defaults_interpolator): + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${missing?}') + + +def test_interpolate_with_existing_required_environment_variable(defaults_interpolator): + assert defaults_interpolator("ok ${FOO?}") == "ok first"