diff --git a/README.md b/README.md index 175eed4..e2b7d82 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Gestalt is an opinionated, strongly typed, configuration library for Python 3.6+. Gestalt aims to simplify configuration loading, getting/setting, and management by letting the user focus on writing programs, and letting Gestalt check configuration types, location, and environment variable overrides. - - ## Install ```python @@ -191,11 +189,13 @@ To set a default configuration value programmatically: ```python g.set_defualt_string('some.default.k', 'default value') ``` + Defined by the function signature ```python set_default_string(key: str, value: str) -> None ``` + The same applies to the remaining types that Gestalt supports: ```python @@ -244,8 +244,16 @@ g.add_vault_config_provider(vault_config, authentication_config) 1. The `vault_config` is a dictionary requiring url, and token 2. The `authentication_config` is a dictionary with Kubernetes `role` and `jwt` -Next, add a path to the secret for Vault to access the cluster which loads it into the running -internal data structure configuration +Raises RuntimeError if the VAULT_ADDR is not set or set incorrectly, or TypeError when client_config is empty. + +Second, add a path to the secret for Vault to access the cluster which loads it into the running +internal data structure configuration. The function `add_vault_secret_path` can consume a +mount point which, if not provided is set to defualt value. ```python -g.add_vault_secret_path('path') \ No newline at end of file +g.add_vault_secret_path(path="your-secret-pat", mount_path="mount-point") +``` + +Lastly, use the `fetch_vault_secret` function to fetch the secrets from your vault cluster. + +Raises a RuntimeError, if the path provided in `add_vault_secret_path` is invalid or the mount is invalid diff --git a/gestalt/__init__.py b/gestalt/__init__.py index 569a7cc..a52b838 100644 --- a/gestalt/__init__.py +++ b/gestalt/__init__.py @@ -2,6 +2,7 @@ import glob import sys import json +import requests import hvac # type: ignore import collections.abc as collections from typing import Dict, List, Tuple, Type, Union, Optional, MutableMapping, Text, Any, NamedTuple @@ -18,7 +19,7 @@ class CACertClient(NamedTuple): class HVAC_ClientConfig(TypedDict): - url: str + url: Optional[str] token: str cert: Union[None, CACertClient] verify: Union[None, bool] @@ -94,19 +95,6 @@ def add_config_path(self, path: str) -> None: f'Given directory path of {tmp} is not a directory') self.__conf_file_paths.append(tmp) - def __init_vault_client(self, url: str, token: str, - cert: Union[Tuple[str, str], None], - verify: Union[bool, str]) -> None: - self.vault_client: hvac.Client = hvac.Client( - url=url, - token=token, - cert=cert, - verify=verify, - ) - - def __authenticate_vault_client(self, role: str, jwt: str) -> None: - self.vault_client.auth_kubernetes(role=role, jwt=jwt) - def add_config_file(self, path: str) -> None: """Adds a path to a single file to read configs from @@ -568,22 +556,34 @@ def add_vault_config_provider( auth_config (HVAC_ClientAuthentication): authenticates the initialized vault client with role and jwt string from kubernetes """ - if client_config['url'] == "": - client_config['url'] = os.environ["VAULT_ADDR"] + if bool(client_config) == False: + raise TypeError("Gestalt Error: client config is empty for Vault") + client_config['url'] = os.environ.get("VAULT_ADDR") + print(client_config) + if not client_config['url']: + raise RuntimeError('Gestalt Error: VAULT_ADDR is missing') if client_config['token'] == "": client_config['token'] == os.environ.get("VAULT_TOKEN", "") if client_config['verify']: verify = True else: verify = False - self.__init_vault_client(client_config['url'], - client_config['token'], - client_config['cert'], - verify=verify) + + self.vault_client: hvac.Client = hvac.Client(client_config['url'], + client_config['token'], + client_config['cert'], + verify=verify) if auth_config: - self.__authenticate_vault_client(auth_config['role'], - auth_config['jwt']) + try: + self.vault_client.auth_kubernetes(\ + role=auth_config['role'], + jwt=auth_config['jwt'] + ) + except requests.exceptions.ConnectionError as err: + print( + "Gestalt Error: Couldn't connect to Vault. Maybe missing VAULT_ADDR?" + ) def add_vault_secret_path(self, path: str, @@ -608,6 +608,14 @@ def fetch_vault_secrets(self) -> None: for vault_path in self.__vault_paths: mount_path = str(vault_path[0]) secret_path = vault_path[1] - secret_token = self.vault_client.secrets.kv.v2.read_secret_version( - mount_point=str(mount_path), path=secret_path) - self.__conf_data.update(secret_token['data']['data']) + try: + secret_token = self.vault_client.secrets.kv.v2.read_secret_version( + mount_point=str(mount_path), path=secret_path) + self.__conf_data.update(secret_token['data']['data']) + except hvac.exceptions.InvalidPath as err: + raise RuntimeError( + "Gestalt Error: The secret path or mount is set incorrectly" + ) + except requests.exceptions.ConnectionError as err: + raise RuntimeError( + "Gestalt Error: Gestalt couldn't connect to Vault") diff --git a/setup.py b/setup.py index 1f0ce4f..288887b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def readme(): setup(name='gestalt-cfg', - version='1.0.8', + version='1.0.9', description='A sensible configuration library for Python', long_description=readme(), long_description_content_type="text/markdown", diff --git a/tests/test_gestalt.py b/tests/test_gestalt.py index 987408e..11fbe93 100644 --- a/tests/test_gestalt.py +++ b/tests/test_gestalt.py @@ -423,7 +423,13 @@ def test_set_default_bad_type_set_config(): assert 'Set config has' in terr -def test_vault_setup(): +## Vault testing +@pytest.fixture +def user_setup(): + os.environ['VAULT_ADDR'] = "http://localhost:8200" + + +def test_vault_setup(user_setup): g = gestalt.Gestalt() client_config = gestalt.HVAC_ClientConfig() client_config['url'] = "" @@ -442,8 +448,15 @@ def test_vault_fail_setup(): client_config['cert'] = None client_config['verify'] = True g.add_vault_config_provider(client_config, auth_config=None) - with pytest.raises(requests.exceptions.MissingSchema): - g.vault_client.is_authenticated() + assert g.vault_client.is_authenticated() == False + + +def test_vault_connection_error(): + g = gestalt.Gestalt() + client_config = gestalt.HVAC_ClientConfig() + with pytest.raises(TypeError): + g.add_vault_config_provider(client_config=client_config, + auth_config=None) def test_vault_fail_kubernetes_auth(): @@ -461,6 +474,7 @@ def test_vault_fail_kubernetes_auth(): def test_vault_get(): + print("If this test fails, please makes sure Vault is running locally") g = gestalt.Gestalt() g.build_config() client_config = gestalt.HVAC_ClientConfig() @@ -478,6 +492,23 @@ def test_vault_get(): assert secret == client_password +def test_vault_incorrect_path(): + g = gestalt.Gestalt() + g.build_config() + client_config = gestalt.HVAC_ClientConfig() + client_config['url'] = "" + client_config['token'] = "myroot" + client_config['cert'] = None + client_config['verify'] = True + g.add_vault_config_provider(client_config, auth_config=None) + print("Requires the user to set a token in the client") + client_id = "random_client" + client_password = "random_password" + g.add_vault_secret_path(path="random_path") + with pytest.raises(RuntimeError): + g.fetch_vault_secrets() + + def test_vault_mount_path(): g = gestalt.Gestalt() g.build_config() @@ -494,3 +525,20 @@ def test_vault_mount_path(): g.fetch_vault_secrets() secret = g.get_string(client_id_mount_path) assert secret == client_password_mount_path + + +def test_vault_incorrect_mount_path(): + g = gestalt.Gestalt() + g.build_config() + client_config = gestalt.HVAC_ClientConfig() + client_config['url'] = "" + client_config['token'] = "myroot" + client_config['cert'] = None + client_config['verify'] = True + g.add_vault_config_provider(client_config, auth_config=None) + print("Requires the user to set a token in the client") + client_id_mount_path = "random_test_mount" + client_password_mount_path = "random_test_mount_password" + g.add_vault_secret_path("test", mount_path="incorrect-mount-point") + with pytest.raises(RuntimeError): + g.fetch_vault_secrets() \ No newline at end of file