mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Secrets handlers support
Add functionality that enables configurable secrets sources, with a hard-coded `env` that provides the existing environmental variable support.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
3
octodns/secret/__init__.py
Normal file
3
octodns/secret/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
11
octodns/secret/base.py
Normal file
11
octodns/secret/base.py
Normal file
@@ -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
|
||||
32
octodns/secret/environ.py
Normal file
32
octodns/secret/environ.py
Normal file
@@ -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
|
||||
7
octodns/secret/exception.py
Normal file
7
octodns/secret/exception.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class SecretException(Exception):
|
||||
pass
|
||||
@@ -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))
|
||||
|
||||
|
||||
31
tests/test_octodns_secret_environ.py
Normal file
31
tests/test_octodns_secret_environ.py
Normal file
@@ -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),
|
||||
)
|
||||
Reference in New Issue
Block a user