-
-
Save nqbao/9a9c22298a76584249501b74410b8475 to your computer and use it in GitHub Desktop.
# Copyright (c) 2018 Bao Nguyen <[email protected]> | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
# ============================================================================== | |
import boto3 | |
from botocore.exceptions import ClientError | |
import datetime | |
class SSMParameterStore(object): | |
""" | |
Provide a dictionary-like interface to access AWS SSM Parameter Store | |
""" | |
def __init__(self, prefix=None, ssm_client=None, ttl=None): | |
self._prefix = (prefix or '').rstrip('/') + '/' | |
self._client = ssm_client or boto3.client('ssm') | |
self._keys = None | |
self._substores = {} | |
self._ttl = ttl | |
def get(self, name, **kwargs): | |
assert name, 'Name can not be empty' | |
if self._keys is None: | |
self.refresh() | |
abs_key = "%s%s" % (self._prefix, name) | |
if name not in self._keys: | |
if 'default' in kwargs: | |
return kwargs['default'] | |
raise KeyError(name) | |
elif self._keys[name]['type'] == 'prefix': | |
if abs_key not in self._substores: | |
store = self.__class__(prefix=abs_key, ssm_client=self._client, ttl=self._ttl) | |
store._keys = self._keys[name]['children'] | |
self._substores[abs_key] = store | |
return self._substores[abs_key] | |
else: | |
return self._get_value(name, abs_key) | |
def refresh(self): | |
self._keys = {} | |
self._substores = {} | |
paginator = self._client.get_paginator('describe_parameters') | |
pager = paginator.paginate( | |
ParameterFilters=[ | |
dict(Key="Path", Option="Recursive", Values=[self._prefix]) | |
] | |
) | |
for page in pager: | |
for p in page['Parameters']: | |
paths = p['Name'][len(self._prefix):].split('/') | |
self._update_keys(self._keys, paths) | |
@classmethod | |
def _update_keys(cls, keys, paths): | |
name = paths[0] | |
# this is a prefix | |
if len(paths) > 1: | |
if name not in keys: | |
keys[name] = {'type': 'prefix', 'children': {}} | |
cls._update_keys(keys[name]['children'], paths[1:]) | |
else: | |
keys[name] = {'type': 'parameter', 'expire': None} | |
def keys(self): | |
if self._keys is None: | |
self.refresh() | |
return self._keys.keys() | |
def _get_value(self, name, abs_key): | |
entry = self._keys[name] | |
# simple ttl | |
if self._ttl == False or (entry['expire'] and entry['expire'] <= datetime.datetime.now()): | |
entry.pop('value', None) | |
if 'value' not in entry: | |
parameter = self._client.get_parameter(Name=abs_key, WithDecryption=True)['Parameter'] | |
value = parameter['Value'] | |
if parameter['Type'] == 'StringList': | |
value = value.split(',') | |
entry['value'] = value | |
if self._ttl: | |
entry['expire'] = datetime.datetime.now() + datetime.timedelta(seconds=self._ttl) | |
else: | |
entry['expire'] = None | |
return entry['value'] | |
def __contains__(self, name): | |
try: | |
self.get(name) | |
return True | |
except: | |
return False | |
def __getitem__(self, name): | |
return self.get(name) | |
def __setitem__(self, key, value): | |
raise NotImplementedError() | |
def __delitem__(self, name): | |
raise NotImplementedError() | |
def __repr__(self): | |
return 'ParameterStore[%s]' % self._prefix |
Thankyou! 💯
First of all, great job! I'm having a small issue as I'm a java guy but a Python noob. I can't seem to set a nested location. If I do this:
store = ssm_parameter_store.SSMParameterStore(prefix='/dev')
my_lambda_store = store('lambda/events)
I get an exception. If I navigate and create a store using each key, I can get what I need out of a store.
Any tips here?
tia
Hi @mmaz2301: You can use it as an array store['lambda']['events']
Thanks so much! I had tried that before, but kind of instinctively did ( ['lambda']['events']). Awesome piece of work!
@nqbao The client exception is never used. Did you forget something?
Using a bare except clause (line 120) is not a good practise. The better alternative is except Exception
.
@nqbao Great work, thanks for sharing!
However this won't work if there is a mixture of top-level parameters (no prefix) and subfolders, as it will cutoff the first letter of the top-level parameters. A fix on line 72 could be:
name = p['Name']
if name.startswith('/'):
paths = name[len(self._prefix):].split('/') # Same as original version, removes the prefix
else:
paths = name.split('/') # If there is no prefix, avoids cutting off first letter of the parameter key
this is great, thank you!! 🙏