Fix environment variable interpolation within configured repository paths (#782).

This commit is contained in:
Dan Helfman 2023-11-03 21:16:04 -07:00
parent 2da43239f6
commit 6cc93c4eb9
4 changed files with 29 additions and 18 deletions

1
NEWS
View file

@ -8,6 +8,7 @@
overriding the existing "archive_name_format" and "match_archives" options in configuration. overriding the existing "archive_name_format" and "match_archives" options in configuration.
* #779: Only parse "--override" values as complex data types when they're for options of those * #779: Only parse "--override" values as complex data types when they're for options of those
types. types.
* #782: Fix environment variable interpolation within configured repository paths.
1.8.4 1.8.4
* #715: Add a monitoring hook for sending backup status to a variety of monitoring services via the * #715: Add a monitoring hook for sending backup status to a variety of monitoring services via the

View file

@ -1,21 +1,22 @@
import os import os
import re import re
_VARIABLE_PATTERN = re.compile( VARIABLE_PATTERN = re.compile(
r'(?P<escape>\\)?(?P<variable>\$\{(?P<name>[A-Za-z0-9_]+)((:?-)(?P<default>[^}]+))?\})' r'(?P<escape>\\)?(?P<variable>\$\{(?P<name>[A-Za-z0-9_]+)((:?-)(?P<default>[^}]+))?\})'
) )
def _resolve_string(matcher): def resolve_string(matcher):
''' '''
Get the value from environment given a matcher containing a name and an optional default value. Given a matcher containing a name and an optional default value, get the value from environment.
If the variable is not defined in environment and no default value is provided, an Error is raised.
Raise ValueError if the variable is not defined in environment and no default value is provided.
''' '''
if matcher.group('escape') is not None: if matcher.group('escape') is not None:
# in case of escaped envvar, unescape it # In the case of an escaped environment variable, unescape it.
return matcher.group('variable') return matcher.group('variable')
# resolve the env var # Resolve the environment variable.
name, default = matcher.group('name'), matcher.group('default') name, default = matcher.group('name'), matcher.group('default')
out = os.getenv(name, default=default) out = os.getenv(name, default=default)
@ -27,19 +28,24 @@ def _resolve_string(matcher):
def resolve_env_variables(item): def resolve_env_variables(item):
''' '''
Resolves variables like or ${FOO} from given configuration with values from process environment Resolves variables like or ${FOO} from given configuration with values from process environment.
Supported formats:
- ${FOO} will return FOO env variable
- ${FOO-bar} or ${FOO:-bar} will return FOO env variable if it exists, else "bar"
If any variable is missing in environment and no default value is provided, an Error is raised. Supported formats:
* ${FOO} will return FOO env variable
* ${FOO-bar} or ${FOO:-bar} will return FOO env variable if it exists, else "bar"
Raise if any variable is missing in environment and no default value is provided.
''' '''
if isinstance(item, str): if isinstance(item, str):
return _VARIABLE_PATTERN.sub(_resolve_string, item) return VARIABLE_PATTERN.sub(resolve_string, item)
if isinstance(item, list): if isinstance(item, list):
for i, subitem in enumerate(item): for index, subitem in enumerate(item):
item[i] = resolve_env_variables(subitem) item[index] = resolve_env_variables(subitem)
if isinstance(item, dict): if isinstance(item, dict):
for key, value in item.items(): for key, value in item.items():
item[key] = resolve_env_variables(value) item[key] = resolve_env_variables(value)
return item return item

View file

@ -110,10 +110,12 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
raise Validation_error(config_filename, (str(error),)) raise Validation_error(config_filename, (str(error),))
override.apply_overrides(config, schema, overrides) override.apply_overrides(config, schema, overrides)
logs = normalize.normalize(config_filename, config)
if resolve_env: if resolve_env:
environment.resolve_env_variables(config) environment.resolve_env_variables(config)
logs = normalize.normalize(config_filename, config)
try: try:
validator = jsonschema.Draft7Validator(schema) validator = jsonschema.Draft7Validator(schema)
except AttributeError: # pragma: no cover except AttributeError: # pragma: no cover

View file

@ -1,4 +1,5 @@
import io import io
import os
import string import string
import sys import sys
@ -244,7 +245,7 @@ def test_parse_configuration_applies_overrides():
assert logs == [] assert logs == []
def test_parse_configuration_applies_normalization(): def test_parse_configuration_applies_normalization_after_environment_variable_interpolation():
mock_config_and_schema( mock_config_and_schema(
''' '''
location: location:
@ -252,17 +253,18 @@ def test_parse_configuration_applies_normalization():
- /home - /home
repositories: repositories:
- path: hostname.borg - ${NO_EXIST:-user@hostname:repo}
exclude_if_present: .nobackup exclude_if_present: .nobackup
''' '''
) )
flexmock(os).should_receive('getenv').replace_with(lambda variable_name, default: default)
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml') config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
assert config == { assert config == {
'source_directories': ['/home'], 'source_directories': ['/home'],
'repositories': [{'path': 'hostname.borg'}], 'repositories': [{'path': 'ssh://user@hostname/./repo'}],
'exclude_if_present': ['.nobackup'], 'exclude_if_present': ['.nobackup'],
} }
assert logs assert logs