Skip to content

Add Support for Google Secret Manager as a Source #168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions decouple.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,53 @@ def __getitem__(self, key):
return self.data[key]


class RepositoryGoogleSecretManager(RepositoryEnv):
"""
Repository class for retrieving configuration options from Google Secret Manager.

This class extends `RepositoryEnv` to specifically handle configurations stored in
Google Secret Manager. It parses strings formatted in a similar way to `.env` files,
converting them into a dictionary of configuration options.

Attributes:
data (dict): A dictionary holding the parsed key-value pairs from the Google
Secret Manager source.
"""

def __init__(self, source):
"""
Initialize RepositoryGoogleSecretManager with a Google Secret Manager source.

The source string is expected to have one "KEY=value" pair per line, akin to
the `.env` file format. Lines beginning with `#` are treated as comments and
are disregarded. Keys and values are trimmed of surrounding whitespace for
accurate parsing.

Args:
source (str): The string source from Google Secret Manager to be parsed.
"""
self.data = {}
source_lines = source.split('\n')

for line in source_lines:
line = line.strip()

if not line or line.startswith('#') or '=' not in line:
continue

key, value = line.split('=', 1)
key = key.strip()
value = value.strip()

if len(value) >= 2 and (
(value[0] == "'" and value[-1] == "'")
or (value[0] == '"' and value[-1] == '"')
):
value = value[1:-1]

self.data[key] = value


class AutoConfig(object):
"""
Autodetects the config file and type.
Expand Down
105 changes: 105 additions & 0 deletions tests/test_gsm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# coding: utf-8
import os
import pytest
from decouple import Config, RepositoryGoogleSecretManager, UndefinedValueError

ENVSTRING = """
KeyTrue=True\nKeyOne=1\nKeyYes=yes
KeyY=y
KeyOn=on

KeyFalse=False
KeyZero=0
KeyNo=no
KeyN=n
KeyOff=off
KeyEmpty=

# CommentedKey=None
KeyWithSpaces = Some Value With Spaces
KeyWithQuotes="Quoted Value"
"""


@pytest.fixture(scope="module")
def config():
return Config(RepositoryGoogleSecretManager(ENVSTRING))


def test_string_comment(config):
with pytest.raises(UndefinedValueError):
config("CommentedKey")


def test_string_bool_true(config):
assert config("KeyTrue", cast=bool)
assert config("KeyOne", cast=bool)
assert config("KeyYes", cast=bool)
assert config("KeyY", cast=bool)
assert config("KeyOn", cast=bool)


def test_string_bool_false(config):
assert not config("KeyFalse", cast=bool)
assert not config("KeyZero", cast=bool)
assert not config("KeyNo", cast=bool)
assert not config("KeyOff", cast=bool)
assert not config("KeyN", cast=bool)
assert not config("KeyEmpty", cast=bool)


def test_string_undefined(config):
with pytest.raises(UndefinedValueError):
config("UndefinedKey")


def test_string_default_none(config):
assert config("UndefinedKey", default=None) is None


def test_string_default_bool(config):
assert not config("UndefinedKey", default=False, cast=bool)
assert config("UndefinedKey", default=True, cast=bool)


def test_string_default(config):
assert not config("UndefinedKey", default=False)
assert config("UndefinedKey", default=True)


def test_string_default_invalid_bool(config):
with pytest.raises(ValueError):
config("UndefinedKey", default="NotBool", cast=bool)


def test_string_empty(config):
assert config("KeyEmpty", default=None) == ""


def test_string_support_space(config):
assert config("KeyWithSpaces") == "Some Value With Spaces"


def test_string_os_environ(config):
os.environ["KeyOverrideByEnv"] = "This"
assert config("KeyOverrideByEnv") == "This"
del os.environ["KeyOverrideByEnv"]


def test_string_undefined_but_present_in_os_environ(config):
os.environ["KeyOnlyEnviron"] = ""
assert config("KeyOnlyEnviron") == ""
del os.environ["KeyOnlyEnviron"]


def test_string_empty_string_means_false(config):
assert not config("KeyEmpty", cast=bool)


def test_string_repo_keyerror(config):
with pytest.raises(KeyError):
config.repository["UndefinedKey"]


def test_string_quoted_value(config):
assert config("KeyWithQuotes") == "Quoted Value"