import hashlib import json import sys from six import string_types class LogicalIdGenerator(object): # NOTE: Changing the length of the hash will change backwards compatibility. This will break the stability contract # given by this class HASH_LENGTH = 10 def __init__(self, prefix, data_obj=None): """ Generate logical IDs for resources that are stable, deterministic and platform independent :param prefix: Prefix for the logicalId :param data_obj: Data object to trigger new changes on. If set to None, this is ignored """ data_str = "" if data_obj: data_str = self._stringify(data_obj) self._prefix = prefix self.data_str = data_str def gen(self): """ Generate stable LogicalIds based on the prefix and given data. This method ensures that the logicalId is deterministic and stable based on input prefix & data object. In other words: logicalId changes *if and only if* either the `prefix` or `data_obj` changes Internally we simply use a SHA1 of the data and append to the prefix to create the logicalId. NOTE: LogicalIDs are how CloudFormation identifies a resource. If this ID changes, CFN will delete and create a new resource. This can be catastrophic for most resources. So it is important to be *always* backwards compatible here. :return: LogicalId that can be used to construct resources :rtype string """ data_hash = self.get_hash() return "{prefix}{hash}".format(prefix=self._prefix, hash=data_hash) def get_hash(self, length=HASH_LENGTH): """ Generate and return a hash of data that can be used as suffix of logicalId :return: Hash of data if it was present :rtype string """ data_hash = "" if not self.data_str: return data_hash encoded_data_str = self.data_str if sys.version_info.major == 2: # In Py2, only unicode needs to be encoded. if isinstance(self.data_str, unicode): encoded_data_str = self.data_str.encode('utf-8') else: # data_str should always be unicode on python 3 encoded_data_str = self.data_str.encode('utf-8') data_hash = hashlib.sha1(encoded_data_str).hexdigest() return data_hash[:length] def _stringify(self, data): """ Stable, platform & language-independent stringification of a data with basic Python type. We use JSON to dump a string instead of `str()` method in order to be language independent. :param data: Data to be stringified. If this is one of JSON native types like string, dict, array etc, it will be properly serialized. Otherwise this method will throw a TypeError for non-JSON serializable objects :return: string representation of the dictionary :rtype string """ if isinstance(data, string_types): return data # Get the most compact dictionary (separators) and sort the keys recursively to get a stable output return json.dumps(data, separators=(',', ':'), sort_keys=True)