diff --git a/README.md b/README.md index a2ef335..492e02c 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Then create a config file for retemplate that contains these elements: template: /etc/retemplate/hiapi.config.j2 owner: root group: root - chmod: 0600 + chmod: "0600" frequency: 60 onchange: supervisorctl restart hiapi @@ -154,7 +154,7 @@ Retemplate needs to know what files to template and various other options about template: /etc/retemplate/hiapi.config.j2 owner: root group: root - chmod: 0600 + chmod: "0600" onchange: supervisorctl restart hiapi frequency: 60 random_offset_max: 30 @@ -164,7 +164,7 @@ Here, `templates` is the root-level option underneath which all of your template * **template**: *The source file, the unrendered template itself.* * **owner**: *After rendering the template, the file will be owned by this user.* * **group**: *After rendering the template, the file will be owned by this group.* -* **chmod**: *After rendering the template, this is the octal-format permission for the file.* +* **chmod**: *After rendering the template, this is the octal-format permission for the file. It must be a string, and due to how YAML interprets numerical values, you must explicitly surround the value with quotes to ensure it is interpreted as a string.* * **onchange**: *If the rendered file differs from what was there before, this is a command that will be executed. This should be used for reloading services after their config files have changed. This value can be omitted if it is unnecessary. Retemplate will emit a log event saying no onchange command is to be run.* * **frequency**: *The number of seconds to wait between template renders.* * **random_offset_max**: *When present, this causes the rendering process to wait an additional amount of time - up to this number of seconds - before it gets underway. This is designed to prevent the alignment of jobs such that they all make API calls or disk access requests simultaneously. If not set, there is no additional time offset.* @@ -187,6 +187,13 @@ They are then referenced in the same way, but without the assignment portion: Referenced variables must be absent of whitespace. +### Default Values +If you want a failure to retrieve a value to not cause a failure to render a template, you can provide a default value by using the special `rtpl_default` argument in the URI. Consider this bit of template: + + rtpl://secrets-manager/key?rtpl_default=Blah + +In this case, if the secrets-manager store fails to find the key, it will use the provided default value of "Blah" instead. + ### Example Let's consider a case where you need to configure an agent with an API key, but that API key varies depending on which environment your system runs in. Further, let's say you have a script on the server that, when run, emits the name of the environment, and that environment name is then used to look up the API key in AWS Secrets Manager. @@ -207,7 +214,7 @@ To accomplish this, you might start with a configuration that looks like this: template: /etc/retemplate/agent.config.ini.j2 owner: agent group: agent - chmod: 0600 + chmod: "0600" onchange: systemctl restart agent frequency: 60 random_offset_max: 30 diff --git a/config.yml.example b/config.yml.example index caa3c6a..c81ca3d 100644 --- a/config.yml.example +++ b/config.yml.example @@ -26,7 +26,7 @@ templates: template: /etc/retemplate/hiapi.config.j2 owner: root group: root - chmod: 0600 + chmod: "0600" onchange: supervisorctl restart hiapi frequency: 60 random_offset_max: 30 diff --git a/retemplate/__init__.py b/retemplate/__init__.py index f982b06..fb8aa5a 100644 --- a/retemplate/__init__.py +++ b/retemplate/__init__.py @@ -12,7 +12,7 @@ import subprocess import sys -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, NoCredentialsError from urllib.parse import urlparse, parse_qs from time import sleep @@ -115,6 +115,9 @@ def get_value(self, key, **kwargs): logging.error('Failed to retrieve secret {}; Error code: {}; Full error: {}'.format( kwargs['SecretId'], ex.response['Error']['Code'], ex)) raise RetrievalError + except NoCredentialsError: + logging.error('Could not retrieve secret because no AWS credentials were supplied') + raise RetrievalError class AwsSystemsManagerStore(DataStore): @@ -157,6 +160,9 @@ def get_value(self, key, **kwargs): logging.error('Failed to retrieve parameter {}; Error code: {}; Full error: {}'.format( kwargs['Name'], e.response['Error']['Code'], e)) raise RetrievalError + except NoCredentialsError: + logging.error('Could not retrieve parameter because no AWS credentials were supplied') + raise RetrievalError class RedisStore(DataStore): @@ -272,6 +278,11 @@ def resolve_value(self, uri): url = urlparse(uri) qs = parse_qs(url.query) + default = None + if 'rtpl_default' in qs: + default = ''.join(qs['rtpl_default']) + # The value must be removed to prevent it from being passed into the data store call + del(qs['rtpl_default']) store = self.stores[url.netloc] key = url.path[1:].strip() # path always starts with a '/'; cut it out @@ -281,8 +292,13 @@ def resolve_value(self, uri): logging.info('Store {} got value \'{}\' for key \'{}\' and query \'{}\''.format( store.name, value, key, url.query)) except RetrievalError: - logging.error('Failed to resolve value for URI: {}'.format(uri)) - raise + if not default: + logging.error('Failed to resolve value for URI: {}'.format(uri)) + raise + else: + logging.error('Failed to resolve value for URI: {}, using default value: {}'.format( + uri, default)) + value = default return value def read_template(self): diff --git a/setup.py b/setup.py index 8b69867..ca94f67 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='retemplate', - version='0.0.10', + version='0.0.11', description="A module to execute a Jinja template on a schedule, supporting several backends for value storage", url='https://github.com/ryanjjung/retemplate', author='Ryan Jung',