Skip to content
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

testing: allow lazy init of boto client #25

Merged
merged 2 commits into from
Jun 28, 2021
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
name: "Python ${{ matrix.python-version }}"
runs-on: "ubuntu-latest"
env:
USING_COVERAGE: '2.7,3.8'
USING_COVERAGE: '3.8'

strategy:
matrix:
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-alpha - 3.10"]
python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-alpha - 3.10"]

steps:
- uses: "actions/checkout@v2"
Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The third digit is only for regressions.
Backward-incompatible changes:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

*none*
- Python 2.7 support dropped


Deprecations:
Expand All @@ -24,7 +24,8 @@ Deprecations:
Changes:
^^^^^^^^

*none*
- Lazily init the AWS SecretsManager client to make unit testing easier.
`#25 <https://github.com/hynek/environ-config/pull/25>`_


----
Expand Down
6 changes: 0 additions & 6 deletions src/environ/_environ_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@

import logging
import os
import sys

import attr

from .exceptions import MissingEnvValueError


PY2 = sys.version_info[0] == 2

CNF_KEY = "environ_config"
log = logging.getLogger(CNF_KEY)

Expand All @@ -38,9 +35,6 @@ def __init__(self, bool_=True):
def __bool__(self):
return self._bool

if PY2:
__nonzero__ = __bool__


PREFIX_NOT_SET = Sentinel(False)
DEFAULT_PREFIX = "APP"
Expand Down
7 changes: 2 additions & 5 deletions src/environ/secrets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

import attr

from environ._environ_config import CNF_KEY, PY2, RAISE, _ConfigEntry
from environ._environ_config import CNF_KEY, RAISE, _ConfigEntry
from environ.exceptions import MissingSecretImplementationError

from ._utils import _get_default_secret, _open_file
Expand All @@ -46,10 +46,7 @@ def secret(self, *args, **kwargs):
log = logging.getLogger(__name__)


if PY2:
FileOpenError = IOError
else:
FileOpenError = OSError
FileOpenError = OSError


@attr.s
Expand Down
20 changes: 15 additions & 5 deletions src/environ/secrets/awssm.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"""
Handling of sensitive data stored in AWS SecretsManager
"""
import functools
import logging

import attr
Expand All @@ -38,7 +37,13 @@ def converter(value):
return converter


@attr.s
def _build_secretsmanager_client():
client = boto3.client("secretsmanager")
log.debug("Created a secretsmanager client %s", client)
return client


@attr.s(init=False)
class SecretsManagerSecrets(object):
"""
Load secrets from the AWS secretsmanager.
Expand All @@ -48,9 +53,14 @@ class SecretsManagerSecrets(object):
.. versionadded:: 21.4.0
"""

client = attr.ib(
default=attr.Factory(functools.partial(boto3.client, "secretsmanager"))
)
def __init__(self, client=None):
self._client = client

@property
def client(self):
if self._client is None:
self._client = _build_secretsmanager_client()
return self._client

def secret(
self,
Expand Down
67 changes: 66 additions & 1 deletion tests/test_secrets_awssm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import uuid

from unittest.mock import patch

import attr
import boto3
import pytest
Expand All @@ -35,7 +38,27 @@ def _shut_boto_up():
logging.getLogger(name).setLevel(logging.WARNING)


@pytest.fixture(name="secretsmanager", autouse=True)
@pytest.fixture(name="force_region", autouse=True, scope="session")
def _force_region():
with patch.dict(os.environ, {"AWS_DEFAULT_REGION": "us-east-1"}):
yield


@pytest.fixture(name="mock_aws_credentials")
def _mock_aws_credentials(force_region):
with patch.dict(
os.environ,
{
"AWS_ACCESS_KEY_ID": "testing",
"AWS_SECRET_ACCESS_KEY": "testing",
"AWS_SECURITY_TOKEN": "testing",
"AWS_SESSION_TOKEN": "testing",
},
):
yield


@pytest.fixture(name="secretsmanager")
def _secretsmanager():
with mock_secretsmanager():
yield boto3.client("secretsmanager", region_name="us-east-2")
Expand Down Expand Up @@ -66,6 +89,48 @@ class Cfg(object):
with pytest.raises(MissingSecretError):
environ.to_config(Cfg, {})

def test_no_default_resolves_secret(self, sm, secretsmanager):
"""
Verify that, when no default is provided, resolution should not happen
until the configuration is needed.
"""

@environ.config
class Cfg(object):
pw = sm.secret()

secretsmanager.create_secret(Name="SecretName")
secretsmanager.put_secret_value(
SecretId="SecretName", SecretString="no-default"
)
assert (
environ.to_config(Cfg, {"APP_PW": "SecretName"}).pw == "no-default"
)

def test_secret_works_with_default_client_overridden(
self, mock_aws_credentials
):
"""
Assert that the SM type can function without a custom client override
when testing
"""
sm = SecretsManagerSecrets()

@environ.config
class Cfg(object):
pw = sm.secret()

with mock_secretsmanager():
# we need to make sure we're using the same region. It doesn't
# matter which -- moto _and_ boto will try figure it out from the
# environment -- but it has to be the same.
sm.client.create_secret(Name="SecretName")
sm.client.put_secret_value(
SecretId="SecretName", SecretString="no-default"
)
conf = environ.to_config(Cfg, {"APP_PW": "SecretName"})
assert conf.pw == "no-default"

def test_default(self, sm, secret):
"""
Defaults are used iff the key is missing.
Expand Down
3 changes: 1 addition & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ filterwarnings =
# Keep docs in sync with docs env and .readthedocs.yml.
[gh-actions]
python =
2.7: py27
3.5: py35
3.6: py36
3.7: py37, docs
Expand All @@ -20,7 +19,7 @@ python =


[tox]
envlist = lint,py27,py35,py36,py37,py38-oldestAttrs,py38,py39,py310,manifest,docs,pypi-description,coverage-report
envlist = lint,py35,py36,py37,py38-oldestAttrs,py38,py39,py310,manifest,docs,pypi-description,coverage-report
isolated_build = true

[testenv:lint]
Expand Down