Skip to content

Commit

Permalink
Merge pull request #362 from kooba/env_var_configs
Browse files Browse the repository at this point in the history
Basic support for environment variables.
  • Loading branch information
mattbennett committed Oct 10, 2016
2 parents 37298fe + 3d4976f commit 3151d7c
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 1 deletion.
25 changes: 25 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ and providing a simple YAML configuration file:
The ``LOGGING`` entry is passed to :func:`logging.config.dictConfig` and should conform to the schema for that call.


Environment variable substitution
---------------------------------
YAML configuration files have basic support for environment variables.
You can use bash style syntax: ``${ENV_VAR}``
Optionally you can provide default values ``${ENV_VAR:default_value}``


.. code-block:: yaml
# foobar.yaml
AMQP_URI: amqp://${RABBITMQ_USER:guest}:${RABBITMQ_PASSWORD:password}@${RABBITMQ_HOST:localhost}
To run your service and set environment variables for it to use:

.. code-block:: shell
$ RABBITMQ_USER=user RABBITMQ_PASSWORD=password RABBITMQ_HOST=host nameko run --config ./foobar.yaml <module>[:<ServiceClass>]
If you need to quote the values in your YAML file, the explicit ``!env_var`` resolver is required:

.. code-block:: yaml
# foobar.yaml
AMQP_URI: !env_var "amqp://${RABBITMQ_USER:guest}:${RABBITMQ_PASSWORD:password}@${RABBITMQ_HOST:localhost}"
Interacting with running services
---------------------------------

Expand Down
37 changes: 37 additions & 0 deletions nameko/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
from __future__ import print_function

import argparse
import os
import re
import yaml

from nameko.exceptions import CommandError, ConfigurationError
from . import backdoor, run, shell

ENV_VAR_MATCHER = re.compile(
r"""
\$\{ # match characters `${` literally
([^}:\s]+) # 1st group: matches any character except `}` or `:`
:? # matches the literal `:` character zero or one times
([^}]+)? # 2nd group: matches any character except `}`
\} # match character `}` literally
""", re.VERBOSE
)
IMPLICIT_ENV_VAR_MATCHER = re.compile(
r"""
.* # matches any number of any characters
\$\{.*\} # matches any number of any characters
# between `${` and `}` literally
.* # matches any number of any characters
""", re.VERBOSE
)


def setup_parser():
parser = argparse.ArgumentParser()
Expand All @@ -19,9 +40,25 @@ def setup_parser():
return parser


def _replace_env_var(match):
env_var, default = match.groups()
return os.environ.get(env_var, default)


def _env_var_constructor(loader, node):
value = loader.construct_scalar(node)
return ENV_VAR_MATCHER.sub(_replace_env_var, value)


def setup_yaml_parser():
yaml.add_constructor('!env_var', _env_var_constructor)
yaml.add_implicit_resolver('!env_var', IMPLICIT_ENV_VAR_MATCHER)


def main():
parser = setup_parser()
args = parser.parse_args()
setup_yaml_parser()
try:
args.main(args)
except (CommandError, ConfigurationError) as exc:
Expand Down
49 changes: 48 additions & 1 deletion test/cli/test_main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import sys

from mock import patch
import os
import pytest
import yaml

from nameko.cli.main import main, setup_parser
from nameko.cli.main import main, setup_parser, setup_yaml_parser
from nameko.exceptions import CommandError, ConfigurationError


Expand Down Expand Up @@ -52,3 +54,48 @@ def test_flag_action(param, value):
args.append(param)
parsed = parser.parse_args(args)
assert parsed.rlwrap is value


class TestConfigEnvironmentVariables(object):

@pytest.mark.parametrize(('yaml_config', 'env_vars', 'expected_config'), [
# no default value, no env value
('FOO: ${BAR}', {}, {'FOO': ''}),
# use default value if env value not provided
('FOO: ${BAR:foo}', {}, {'FOO': 'foo'}),
# use env value
('FOO: ${BAR}', {'BAR': 'bar'}, {'FOO': 'bar'}),
# use env value instead of default
('FOO: ${BAR:foo}', {'BAR': 'bar'}, {'FOO': 'bar'}),
# handles multi line
(
"""
FOO: ${BAR:foo}
BAR: ${FOO:bar}
""",
{'BAR': 'bar', 'FOO': 'foo'},
{'FOO': 'bar', 'BAR': 'foo'}
),
# quoted values don't work without explicit resolver
('FOO: "${BAR:foo}"', {'BAR': 'bar'}, {'FOO': '${BAR:foo}'}),
# quoted values work only with explicit resolver
('FOO: !env_var "${BAR:foo}"', {'BAR': 'bar'}, {'FOO': 'bar'}),
# $ sign can be used
('FOO: $bar', {}, {'FOO': '$bar'}),
# multiple substitutions
(
'FOO: http://${BAR:foo}/${FOO:bar}',
{'BAR': 'bar', 'FOO': 'foo'},
{'FOO': 'http://bar/foo'}),
])
def test_environment_vars_in_config(
self, yaml_config, env_vars, expected_config
):
setup_yaml_parser()

with patch.dict('os.environ'):
for key, val in env_vars.items():
os.environ[key] = val

results = yaml.load(yaml_config)
assert results == expected_config

0 comments on commit 3151d7c

Please sign in to comment.