Skip to content

Commit

Permalink
Source template variable restrictions (#133)
Browse files Browse the repository at this point in the history
* Source template variable restrictions

Allows use of a dotenv template file to determine the list of
non-prefixed and optionally required variable names.

* Reduce source template code complexity
  • Loading branch information
leijou committed Aug 11, 2020
1 parent c0fe98e commit e6c1ea8
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 3 deletions.
39 changes: 39 additions & 0 deletions README.md
Expand Up @@ -82,6 +82,45 @@ VAR=abc
Any number of `--strict` flags can be provided.
No more forgotten template overrides or missing env vars!

### Source templates

You can use an env template as a source template by using the `-s` or `--source` argument. This will restrict any non-prefixed variables found in the environment to only those already defined in your template.

```bash
$ cat template.env
ANSWER=13
TOKEN=very secret string
VALUE=0
```

```bash
$ export ANSWER='42'
$ dump-env --source=template.env
ANSWER=42
TOKEN=very secret string
VALUE=0
```

You can still also use prefixes to add extra variables from the environment

```bash
$ export EXTRA_VAR='foo'
$ dump-env -s template.env -p EXTRA_
ANSWER=13
TOKEN=very secret string
VALUE=0
VAR=foo
```

#### Strict Source

Using the `--strict-source` flag has the same effect as defining a `--strict` flag for every variable defined in the source template.

```bash
$ export ANSWER='42'
$ dump-env -s template.env --strict-source
Missing env vars: TOKEN, VALUE
```

## Creating secret variables in some CIs

Expand Down
34 changes: 33 additions & 1 deletion dump_env/cli.py
Expand Up @@ -30,6 +30,18 @@ def _create_parser() -> argparse.ArgumentParser:
action='append',
help='Strict variables should exists in os envs',
)
parser.add_argument(
'-s',
'--source',
default='',
type=str,
help='Source template path, restricts non-prefixed env vars',
)
parser.add_argument(
'--strict-source',
action='store_true',
help='All source template variables should exist in os envs',
)
return parser


Expand Down Expand Up @@ -70,12 +82,32 @@ def main() -> NoReturn:
$ dump-env --strict=REQUIRED
This example will dump everything from a source ``.env.template`` file
with only env variables that are defined in the file:
.. code:: bash
$ dump-env -s .env.template
This example will fail if any keys in the source template do not exist
in the environment:
.. code:: bash
$ dump-env -s .env.template --strict-source
"""
args = _create_parser().parse_args()
strict_vars = set(args.strict) if args.strict else None

try:
variables = dumper.dump(args.template, args.prefix, strict_vars)
variables = dumper.dump(
args.template,
args.prefix,
strict_vars,
args.source,
args.strict_source,
)
except StrictEnvException as exc:
sys.stderr.write('{0}\n'.format(str(exc)))
sys.exit(1)
Expand Down
48 changes: 46 additions & 2 deletions dump_env/dumper.py
Expand Up @@ -8,6 +8,8 @@

Store = Mapping[str, str]

EMPTY_STRING = ''


def _parse(source: str) -> Store:
"""
Expand Down Expand Up @@ -58,6 +60,20 @@ def _preload_existing_vars(prefix: str) -> Store:
return prefixed


def _preload_specific_vars(env_keys: Set[str]) -> Store:
"""Preloads env vars from environ in the given set."""
specified = {}

for env_name, env_value in environ.items():
if env_name not in env_keys:
# Skip vars that have not been requested.
continue

specified[env_name] = env_value

return specified


def _assert_envs_exist(strict_keys: Set[str]) -> None:
"""Checks that all variables from strict keys do exists."""
missing_keys: List[str] = [
Expand All @@ -72,10 +88,26 @@ def _assert_envs_exist(strict_keys: Set[str]) -> None:
)


def _source(source: str, strict_source: bool) -> Store:
"""Applies vars and assertions from source template ``.env`` file."""
sourced: Dict[str, str] = {}

sourced.update(_parse(source))

if strict_source:
_assert_envs_exist(set(sourced.keys()))

sourced.update(_preload_specific_vars(set(sourced.keys())))

return sourced


def dump(
template: str = '',
template: str = EMPTY_STRING,
prefixes: Optional[List[str]] = None,
strict_keys: Optional[Set[str]] = None,
source: str = EMPTY_STRING,
strict_source: bool = False,
) -> Dict[str, str]:
"""
This function is used to dump ``.env`` files.
Expand All @@ -93,6 +125,14 @@ def dump(
strict_keys: List of keys that must be presented in env vars.
source: The path of the ``.env`` template file,
defines the base list of env vars that should be checked,
disables the fetching of non-prefixed env vars,
use an empty string when there is no source file.
strict_source: Whether all keys in source template must also be
presented in env vars.
Returns:
Ordered key-value pairs of dumped env and template variables.
Expand All @@ -101,13 +141,17 @@ def dump(
"""
if prefixes is None:
prefixes = ['']
prefixes = [] if source else [EMPTY_STRING]

if strict_keys:
_assert_envs_exist(strict_keys)

store: Dict[str, str] = {}

if source:
# Loading env values from source template file:
store.update(_source(source, strict_source))

if template:
# Loading env values from template file:
store.update(_parse(template))
Expand Down
46 changes: 46 additions & 0 deletions tests/test_cli/test_source.py
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-

import delegator


def test_source_vars(monkeypatch, env_file):
"""Check that cli shows only source variables."""
monkeypatch.setenv('NORMAL_KEY', '1')
monkeypatch.setenv('EXTRA_VALUE', '2')

variables = delegator.run('dump-env -s {0}'.format(env_file))
assert variables.out == 'NORMAL_KEY=1\n'
assert variables.subprocess.returncode == 0


def test_source_prefixes(monkeypatch, env_file):
"""Check that cli allows prefixes with source."""
monkeypatch.setenv('NORMAL_KEY', '1')
monkeypatch.setenv('EXTRA_VALUE', '2')

variables = delegator.run('dump-env -p EXTRA_ -s {0}'.format(env_file))
assert variables.out == 'NORMAL_KEY=1\nVALUE=2\n'
assert variables.subprocess.returncode == 0


def test_source_strict(monkeypatch, env_file):
"""Check that cli works correctly with strict-source."""
monkeypatch.setenv('NORMAL_KEY', '1')
monkeypatch.setenv('EXTRA_VALUE', '2')

variables = delegator.run(
'dump-env --strict-source -s {0}'.format(env_file),
)
assert variables.out == 'NORMAL_KEY=1\n'
assert variables.subprocess.returncode == 0


def test_source_strict_fail(monkeypatch, env_file):
"""Check that cli works correctly with strict-source missing keys."""
monkeypatch.setenv('EXTRA_VALUE', '2')

variables = delegator.run(
'dump-env --strict-source -s {0}'.format(env_file),
)
assert variables.err == 'Missing env vars: NORMAL_KEY\n'
assert variables.subprocess.returncode == 1

0 comments on commit e6c1ea8

Please sign in to comment.