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

add caching for ssm parameters and datadog values #124

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
87 changes: 59 additions & 28 deletions almdrlib/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,19 @@
import boto3
import os
import botocore
import cachetools
import logging


logger = logging.getLogger(__name__)


class AlEnvException(Exception):
pass


class AlEnvAwsConfigurationException(AlEnvException):
pass


class AlEnvConfigurationTableUnavailableException(AlEnvException):
pass

Expand All @@ -64,32 +67,23 @@ class AlmdrlibSourceNotEnabledError(AlEnvException):


class AlEnv:

dynamodb = None
ssm = None
table = None
table_name = None

def __init__(self, application_name, client=None, source=("dynamodb",)):
self.application_name = application_name
self.client = client
self.source = source if type(source) in [tuple, list] else (source,)
self.region = AlEnv._get_region()
self.stack_name = AlEnv._get_stack_name()
self.table_name = AlEnv._table_name(self.region, self.stack_name)
try:
self.dynamodb = boto3.resource('dynamodb')
self.ssm = boto3.client('ssm')
except (botocore.exceptions.NoRegionError, botocore.exceptions.NoCredentialsError) as e:
raise AlEnvAwsConfigurationException(f'Please validate your AWS configuration') from e
if "dynamodb" in source:
try:
self.table = self.dynamodb.Table(self.table_name)
self._table_date_time = self.table.creation_date_time
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
raise AlEnvConfigurationTableUnavailableException(self.table_name)
else:
raise AlEnvException() from e

self._setup_aws_resources(source)

def get(self, key, default=None, format='decoded', type=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be get_from_dynamodb, and the other should be get_from_ssm - as it's not clear just looking at

self._access_key_id = env.get_parameter('access_key_id', decrypt=True)
self._secret_key = env.get_parameter('secret_access_key', decrypt=True)
except Exception as e:
logger.debug(f"Did not initialise aims credentials via SSM for {self._service_name} because {e}")
if self._access_key_id is None or self._secret_key is None:
try:
# if that doesn't work, attempt dynamodb
env = AlEnv(self._service_name, "aims_authc", "dynamodb")
self._access_key_id = env.get('access_key_id')
self._secret_key = env.get('secret_access_key')
why one uses env.get and one uses env.get_parameter

if "dynamodb" not in self.source:
raise AlmdrlibSourceNotEnabledError("dynamodb is not enabled for this environment")
fetched_value = self.table.get_item(Key={"key": self._make_ddb_key(key)}).get('Item', {}).get('value')
fetched_value = AlEnv._get_cached_dynamodb_item(self._make_ddb_key(key))
converted = AlEnv._convert(fetched_value, format, type)
if converted is not None:
return converted
Expand All @@ -99,16 +93,10 @@ def get(self, key, default=None, format='decoded', type=None):
def get_parameter(self, key, default=None, decrypt=False):
if "ssm" not in self.source:
raise AlmdrlibSourceNotEnabledError("ssm is not enabled for this environment")
try:
parameter = self.ssm.get_parameter(Name=self._make_ssm_key(key), WithDecryption=decrypt)
except self.ssm.exceptions.ParameterNotFound:
return default
except botocore.exceptions.ClientError as e:
raise AlEnvException() from e
return parameter["Parameter"]["Value"]
return AlEnv._get_cached_ssm_parameter(self._make_ssm_key(key), default, decrypt)

def _make_ssm_key(self, option_key):
return f"/deployments/{self.stack_name}/{self._get_region()}/env-settings/{self.application_name}/{self._make_client_option_key(option_key)}"
return f"/deployments/{self._get_stack_name()}/{self._get_region()}/env-settings/{self.application_name}/{self._make_client_option_key(option_key)}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding to https://github.com/alertlogic/alertlogic-sdk-python#readme the paths we search for in dynamodb / ssm.

Also, it's probably future work, but it would be nice to be able to specify those paths in ~/.alertlogic/config


def _make_ddb_key(self, option_key):
return f"{self.application_name}.{self._make_client_option_key(option_key)}"
Expand All @@ -118,7 +106,50 @@ def _make_client_option_key(self, option_key):
return option_key
else:
return f"{self.client}.{option_key}"

@staticmethod
@cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=3600))
def _get_cached_ssm_parameter(ssm_key, default=None, decrypt=False):
try:
logger.debug(f"Query SSM for parameter {ssm_key}")
parameter = AlEnv.ssm.get_parameter(Name=ssm_key, WithDecryption=decrypt)
except AlEnv.ssm.exceptions.ParameterNotFound:
return default
except botocore.exceptions.ClientError as e:
raise AlEnvException() from e
return parameter["Parameter"]["Value"]

@staticmethod
@cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=3600))
def _get_cached_dynamodb_item(key):
logger.debug(f"Query DynamoDB table {AlEnv.table_name} for item {key}")
return AlEnv.table.get_item(Key={"key": key}).get('Item', {}).get('value')

@staticmethod
def _setup_aws_resources(source):
try:
if "dynamodb" in source:
if AlEnv.dynamodb is None:
AlEnv.dynamodb = boto3.resource('dynamodb')
if AlEnv.table is None:
AlEnv._setup_dynamodb_table()
AlEnv.ssm = boto3.client('ssm')
except (botocore.exceptions.NoRegionError, botocore.exceptions.NoCredentialsError) as e:
raise AlEnvAwsConfigurationException(f'Please validate your AWS configuration') from e

@staticmethod
def _setup_dynamodb_table():
region = AlEnv._get_region()
stack_name = AlEnv._get_stack_name()
AlEnv.table_name = AlEnv._table_name(region, stack_name)
try:
AlEnv.table = AlEnv.dynamodb.Table(AlEnv.table_name)
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
raise AlEnvConfigurationTableUnavailableException(AlEnv.table_name)
else:
raise AlEnvException() from e

@staticmethod
def _convert(value, format, type):
if format == 'raw' and value:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pyyaml==5.4.1
jsonschema[format_nongpl]==3.2.0
m2r2==0.3.2
boto3>=1.16.57
cachetools>=5.3.0
markupsafe==2.0.1
sphinxcontrib-contentui
sphinx_paramlinks
3 changes: 1 addition & 2 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,14 @@ def get_parameter(**kwargs):
else:
raise MockBotoSSM.exceptions.ParameterNotFound()


class TestAlEnv(unittest.TestCase):
def test_something(self):
boto3.resource = MagicMock(return_value=MockBotoDDB)
boto3.client = MagicMock(return_value=MockBotoSSM)
os.environ['ALERTLOGIC_STACK_REGION'] = 'us-west-1'
os.environ['ALERTLOGIC_STACK_NAME'] = 'production'
env = AlEnv("someapplication", source="dynamodb")
assert env.table_name == 'us-west-1.production.dev.global.settings'
assert AlEnv.table_name == 'us-west-1.production.dev.global.settings'
assert env.get("strkey") == 'strvalue'
assert env.get("strkey", format='raw') == '"strvalue"'
assert env.get("intkey") == '1'
Expand Down