diff --git a/octodns/manager.py b/octodns/manager.py index 7b26f83..d4d23bf 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -10,7 +10,6 @@ from importlib.metadata import PackageNotFoundError from importlib.metadata import version as module_version from json import dumps from logging import getLogger -from os import environ from sys import stdout from . import __version__ @@ -20,6 +19,7 @@ from .processor.meta import MetaProcessor from .provider.base import BaseProvider from .provider.plan import Plan from .provider.yaml import SplitYamlProvider, YamlProvider +from .secret.environ import EnvironSecrets from .yaml import safe_load from .zone import Zone @@ -119,6 +119,8 @@ class Manager(object): manager_config, enable_checksum ) + self.secret_handlers = {'env': EnvironSecrets('env')} + self.auto_arpa = self._config_auto_arpa(manager_config, auto_arpa) self.global_processors = manager_config.get('processors', []) @@ -377,22 +379,21 @@ class Manager(object): if isinstance(v, dict): v = self._build_kwargs(v) elif isinstance(v, str): - if v.startswith('env/'): - # expand env variables + if '/' in v: + handler, name = v.split('/', 1) try: - env_var = v[4:] - v = environ[env_var] + handler = self.secret_handlers[handler] except KeyError: - self.log.exception('Invalid provider config') - raise ManagerException( - f'Incorrect provider config, missing env var {env_var}, {source.context}' + # we don't have a matching handler, but don't want to + # make that an error b/c config values will often + # contain /. We don't want to print the values in case + # they're sensitive so just provide the key, and even + # that only at debug level. + self.log.debug( + '_build_kwargs: no handler found for the value of {k}' ) - try: - # try converting the value to a number to see if it - # converts - v = float(v) - except ValueError: - pass + else: + v = handler.fetch(name, source) kwargs[k] = v diff --git a/octodns/secret/__init__.py b/octodns/secret/__init__.py new file mode 100644 index 0000000..407eb4e --- /dev/null +++ b/octodns/secret/__init__.py @@ -0,0 +1,3 @@ +# +# +# diff --git a/octodns/secret/base.py b/octodns/secret/base.py new file mode 100644 index 0000000..bb7a93b --- /dev/null +++ b/octodns/secret/base.py @@ -0,0 +1,11 @@ +# +# +# + +from logging import getLogger + + +class BaseSecrets: + def __init__(self, name): + self.log = getLogger(f'{self.__class__.__name__}[{name}]') + self.name = name diff --git a/octodns/secret/environ.py b/octodns/secret/environ.py new file mode 100644 index 0000000..ddcb83c --- /dev/null +++ b/octodns/secret/environ.py @@ -0,0 +1,32 @@ +# +# +# + +from os import environ + +from .base import BaseSecrets +from .exception import SecretException + + +class EnvironSecretException(SecretException): + pass + + +class EnvironSecrets(BaseSecrets): + def fetch(self, name, source): + # expand env variables + try: + v = environ[name] + except KeyError: + self.log.exception('Invalid provider config') + raise EnvironSecretException( + f'Incorrect provider config, missing env var {name}, {source.context}' + ) + try: + # try converting the value to a number to see if it + # converts + v = float(v) + except ValueError: + pass + + return v diff --git a/octodns/secret/exception.py b/octodns/secret/exception.py new file mode 100644 index 0000000..3a64c86 --- /dev/null +++ b/octodns/secret/exception.py @@ -0,0 +1,7 @@ +# +# +# + + +class SecretException(Exception): + pass diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 5283769..008f23a 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -26,6 +26,7 @@ from octodns.manager import ( ) from octodns.processor.base import BaseProcessor from octodns.record import Create, Delete, Record, Update +from octodns.secret.environ import EnvironSecretException from octodns.yaml import safe_load from octodns.zone import Zone @@ -68,7 +69,8 @@ class TestManager(TestCase): self.assertTrue('provider config' in str(ctx.exception)) def test_missing_env_config(self): - with self.assertRaises(ManagerException) as ctx: + # details of the EnvironSecrets will be tested in dedicated tests + with self.assertRaises(EnvironSecretException) as ctx: Manager(get_config_filename('missing-provider-env.yaml')).sync() self.assertTrue('missing env var' in str(ctx.exception)) diff --git a/tests/test_octodns_secret_environ.py b/tests/test_octodns_secret_environ.py new file mode 100644 index 0000000..6b97aec --- /dev/null +++ b/tests/test_octodns_secret_environ.py @@ -0,0 +1,31 @@ +# +# +# + +from os import environ +from unittest import TestCase + +from octodns.context import ContextDict +from octodns.secret.environ import EnvironSecretException, EnvironSecrets + + +class TestEnvironSecrets(TestCase): + def test_environ_secrets(self): + # put some secrets into our env + environ['THIS_EXISTS'] = 'and has a val' + environ['THIS_IS_AN_INT'] = '42' + environ['THIS_IS_A_FLOAT'] = '43.44' + + es = EnvironSecrets('env') + + source = ContextDict({}, context='xyz') + self.assertEqual('and has a val', es.fetch('THIS_EXISTS', source)) + self.assertEqual(42, es.fetch('THIS_IS_AN_INT', source)) + self.assertEqual(43.44, es.fetch('THIS_IS_A_FLOAT', source)) + + with self.assertRaises(EnvironSecretException) as ctx: + es.fetch('DOES_NOT_EXIST', source) + self.assertEqual( + 'Incorrect provider config, missing env var DOES_NOT_EXIST, xyz', + str(ctx.exception), + )