def _validate_auth(self): """Method for validating the authentication dictionary retrieved from AWS Parameter Store Returns: bool: Indicator of successful validation """ if not self._config: raise AppIntegrationConfigError( 'Config for service \'{}\' is empty'.format(self.type())) # The config validates that the 'auth' dict was loaded, but do a safety check here if not self._config.auth: raise AppIntegrationConfigError( 'Auth config for service \'{}\' is empty'.format(self.type())) # Get the required authentication keys from the info returned by the subclass required_keys = set(self.required_auth_info()) auth_key_diff = required_keys.difference(set(self._config.auth)) if not auth_key_diff: return missing_auth_keys = ', '.join('\'{}\''.format(key) for key in auth_key_diff) raise AppIntegrationConfigError( 'Auth config for service \'{}\' is missing the following ' 'required keys: {}'.format(self.type(), missing_auth_keys))
def evaluate_interval(self): """Get the interval at which this function is executing. This translates an AWS Rate Schedule Expression ('rate(2 hours)') into a second interval """ if 'interval' not in self: raise AppIntegrationConfigError( 'The \'interval\' value is not defined in the config') rate_match = AWS_RATE_RE.match(self['interval']) if not rate_match: raise AppIntegrationConfigError('Invalid \'rate\' interval value: ' '{}'.format(self['interval'])) value = rate_match.group(2) or rate_match.group(4) unit = rate_match.group(3) or rate_match.group(5).replace('s', '') translate_to_seconds = { 'minute': 60, 'hour': 60 * 60, 'day': 60 * 60 * 24 } interval = int(value) * translate_to_seconds[unit] LOGGER.debug('Evaluated rate interval: %d seconds', interval) # Get the total seconds that this rate evaluates to return interval
def _get_parameters(names): """Simple helper function to house the boto3 ssm client get_parameters operations Args: names (list): A list of parameter names to retrieve from the aws ssm parameter store Returns: tuple (dict, list): Dictionary with the load parameter names as keys and the actual parameter (as a dictionary) as the value. The seconary list that is returned contains any invalid parameters that were not loaded """ LOGGER.debug('Retrieving values from parameter store with names: %s', ', '.join('\'{}\''.format(name) for name in names)) try: parameters = AppConfig.SSM_CLIENT.get_parameters( Names=names, WithDecryption=True) except ClientError as err: joined_names = ', '.join('\'{}\''.format(name) for name in names) raise AppIntegrationConfigError( 'Could not get parameter with names {}. Error: ' '{}'.format(joined_names, err.response['Error']['Message'])) decoded_params = {} for param in parameters['Parameters']: try: decoded_params[param['Name']] = json.loads(param['Value']) except ValueError: raise AppIntegrationConfigError( 'Could not load value for parameter with ' 'name \'{}\'. The value is not valid json: ' '\'{}\''.format(param['Name'], param['Value'])) return decoded_params, parameters['InvalidParameters']
def _validate_config(self): """Validate the top level of the config to make sure it has all the right keys Raises: AppIntegrationConfigError: If the config is invalid, this exception is raised """ if not self: raise AppIntegrationConfigError('App config is empty') required_keys = self.required_base_config_keys() required_keys.update({'region', 'account_id', 'function_name', 'qualifier', 'auth'}) config_key_diff = required_keys.difference(set(self)) if not config_key_diff: return missing_config_keys = ', '.join('\'{}\''.format(key) for key in config_key_diff) raise AppIntegrationConfigError('App config is missing the following required ' 'keys: {}'.format(missing_config_keys))
def load_config(cls, context, event): """Load the configuration for this app invocation Args: context (LambdaContext): The AWS LambdaContext object, passed in via the handler. Returns: AppConfig: Subclassed dictionary with the below structure that contains all of the methods for configuration validation, updating, saving, etc: { 'type': <type>, 'cluster': <cluster>, 'prefix': <prefix>, 'app_name': <app_name>, 'interval': <rate_interval>, 'region': <aws_region>, 'account_id': <aws_account_id>, 'function_name': <function_name>, 'qualifier': <qualifier>, 'last_timestamp': <time>, 'current_state': <running|succeeded|failed>, 'auth': { 'req_auth_item_01': <req_auth_value_01> } } """ # Load the base config from the context that will get updated with other info base_config = AppConfig._parse_context(context) LOGGER.debug('Loaded env config: %s', base_config) # Create the ssm boto3 client that will be cached and used throughout this execution # if one does not exist already if AppConfig.SSM_CLIENT is None: AppConfig.SSM_CLIENT = boto3.client( 'ssm', region_name=base_config['region']) # Generate a map of all the suffixes and full parameter names param_names = { key: '_'.join([base_config['function_name'], key]) for key in { cls.AUTH_CONFIG_SUFFIX, cls.BASE_CONFIG_SUFFIX, cls.STATE_CONFIG_SUFFIX } } LOGGER.debug('Parameter suffixes and names: %s', param_names) # Get the loaded parameters and a list of any invalid ones from parameter store params, invalid_params = AppConfig._get_parameters( param_names.values()) LOGGER.debug( 'Retrieved parameters from parameter store: %s', cls._scrub_auth_info(params, param_names[cls.AUTH_CONFIG_SUFFIX])) LOGGER.debug( 'Invalid parameters could not be retrieved from parameter store: %s', invalid_params) # Check to see if there are any required parameters in the invalid params list missing_required_params = [ param for param in invalid_params if param != param_names[cls.STATE_CONFIG_SUFFIX] ] if missing_required_params: joined_params = ', '.join('\'{}\''.format(param) for param in missing_required_params) raise AppIntegrationConfigError( 'Could not load parameters required for this ' 'configuration: {}'.format(joined_params)) # Update the env config with the base config values base_config.update(params[param_names[cls.BASE_CONFIG_SUFFIX]]) # The state config can be None with first time deploys, so us a lookup and # add default empty values if there is no state found base_config.update( params.get(param_names[cls.STATE_CONFIG_SUFFIX], { cls._STATE_KEY: None, cls._TIME_KEY: None })) # Add the auth config info to the 'auth' key since these key/values can vary # from service to service base_config[cls.AUTH_CONFIG_SUFFIX] = { key: value.encode('utf-8') if isinstance(value, unicode) else value for key, value in params[param_names[ cls.AUTH_CONFIG_SUFFIX]].iteritems() } return AppConfig(base_config, event)