def write_error(text): """ Writes the given text to the console """ console_write("{} {}: {}".format(Defaults().warning_icon, Defaults().failure_colour("ERROR"), Defaults().warning_colour(text)))
def look_for_bdd_tags(_step_obj): _step_obj.context.no_failure = False _step_obj.context.failure_class = None if hasattr(_step_obj, 'all_tags') and len(_step_obj.all_tags) > 0: _step_obj.context.case_insensitivity = True for tag in _step_obj.all_tags: if tag.name.lower() in Defaults().no_failure_tags: _step_obj.context.no_failure = True _step_obj.context.failure_class = tag.name elif tag.name.lower() in Defaults().case_sensitive_tags: _step_obj.context.case_insensitivity = False return _step_obj
def i_action_them(_step_obj, action_type): if action_type == "count": # WARNING: Only case where we set stash as a dictionary, instead of a list. if isinstance(_step_obj.context.stash, list): # This means we are directly started counting without drilling down any property # Thus, our target for the count is stash itself. if _step_obj.context.property_name in Defaults().types_list: _step_obj.context.stash = dict( values=len(_step_obj.context.stash)) else: if isinstance(_step_obj.context.stash[0], dict): if _step_obj.context.stash[0].get('values'): _step_obj.context.stash = seek_key_in_dict( _step_obj.context.stash, 'values') count = 0 for result in _step_obj.context.stash: count += len( result.get('values', {}) ) if result.get('values') and not isinstance( result.get('values'), (int, bool, str)) else 1 _step_obj.context.stash = dict(values=count) else: _step_obj.context.stash = dict( values=len(_step_obj.context.stash)) else: raise TerraformComplianceNotImplemented( 'Invalid action_type in the scenario: {}'.format(action_type))
def look_for_bdd_tags(_step_obj): _step_obj.context.no_failure = False _step_obj.context.failure_class = None _step_obj.context.no_skip = False _step_obj.context.skip_class = None # to pick a tagname that user used _step_obj.context.lines_to_noskip = [] _step_obj.context.bad_tags = False defaults = Defaults() if hasattr(_step_obj, 'all_tags') and len(_step_obj.all_tags) > 0: _step_obj.context.case_insensitivity = True for tag in _step_obj.all_tags: if tag.name.lower() in defaults.no_failure_tags: _step_obj.context.no_failure = True _step_obj.context.failure_class = tag.name elif tag.name.lower() in defaults.case_sensitive_tags: _step_obj.context.case_insensitivity = False elif tag.name.lower() in defaults.no_skip_tags: _step_obj.context.no_skip = True _step_obj.context.skip_class = tag.name _step_obj.context.lines_to_noskip = [-1] elif re.search(r'({})_at_lines?_.*'.format('|'.join(defaults.no_skip_tags)), tag.name.lower()): # check for '@noskip at line x' regex = r'({})_at_lines?((_\d*)*)'.format('|'.join(defaults.no_skip_tags)) m = re.search(regex, tag.name.lower()) if m is not None: _step_obj.context.no_skip = True _step_obj.context.skip_class = tag.name line_numbers_string = m.group(2) try: line_numbers = map(int, line_numbers_string.strip('_').split('_')) if _step_obj.context.lines_to_noskip != [-1]: # no need to worry about this tag if we already have a general noskip _step_obj.context.lines_to_noskip.extend(line_numbers) except Exception as e: Error(_step_obj, f'A tag was determined to be a noskip, but line numbers could not be grouped by the regex {regex}\n{e}') if _step_obj.context.no_failure and _step_obj.context.no_skip: _step_obj.context.no_failure = False _step_obj.context.bad_tags = True Error(_step_obj, f'@{_step_obj.context.failure_class} and @{_step_obj.context.skip_class} tags can not be used at the same time.') ## set the match here case_sensitive = True if hasattr(_step_obj.context, 'case_insensitivity') and not _step_obj.context.case_insensitivity else False _step_obj.context.match = Match(case_sensitive=case_sensitive) return _step_obj
def skip_step(step, resource=None, message=None): if resource is None: resource = 'any' if message is None: message = '{} {} {}'.format(Defaults().yellow('Can not find'), Defaults().green(resource), Defaults().yellow('defined in target terraform plan.')) else: message = Defaults().yellow(message) if str(world.config.formatter) in ('gherkin'): console_write("\t{} {}: {}".format(Defaults().info_icon, Defaults().skip_colour('SKIPPING'), message.format(resource=Defaults().green(resource))) ) step.skip() # Skip all steps in the scenario for each in step.parent.all_steps: each.runable = False
def _mount_references(self): ''' Find the references that is defined in self.configuration :return: ''' self.resources_raw = deepcopy(self.resources) invalid_references = ('var.', 'each.') # This section will link resources found in configuration part of the plan output. # The reference should be on both ways (A->B, B->A) since terraform sometimes report these references # in opposite ways, depending on the provider structure. for resource in self.configuration['resources']: if 'expressions' in self.configuration['resources'][resource]: ref_list = {} for key, value in self.configuration['resources'][resource][ 'expressions'].items(): references = seek_key_in_dict( value, 'references') if isinstance( value, (dict, list)) else [] valid_references = [] for ref in references: if isinstance(ref, dict) and ref.get('references'): valid_references = [ r for r in ref['references'] if not r.startswith(invalid_references) ] for ref in valid_references: # if ref is not in the correct format, handle it if len(ref.split('.')) < 3 and ref.startswith( 'module'): # Using for_each and modules together may introduce an issue where the plan.out.json won't include the necessary third part of the reference # It is partially resolved by mounting the reference to all instances belonging to the module if 'for_each_expression' in self.configuration[ 'resources'][resource]: # extract source resources assumed_source_resources = [ k for k in self.resources.keys() if k.startswith(resource) ] # extract for_each keys assumed_for_each_keys = [ k[len(resource):].split('.')[0] for k in assumed_source_resources ] # combine ref with for each keys assumed_refs = [ '{}{}'.format(ref, key) for key in assumed_for_each_keys ] # get all the resources that start with updated ref ambigious_references = [] for r in self.resources.keys(): for assumed_ref in assumed_refs: if r.startswith(assumed_ref): if key in ref_list: ref_list[key].append(r) else: ref_list[key] = [r] ambigious_references.append(r) # throw a warning defaults = Defaults() console_write('{} {}: {}'.format( defaults.warning_icon, defaults.warning_colour( 'WARNING (Mounting)'), defaults.info_colour( 'The reference "{}" in resource {} is ambigious.' ' It will be mounted to the following resources:' ).format(ref, resource))) for i, r in enumerate(ambigious_references, 1): console_write( defaults.info_colour('{}. {}'.format( i, r))) # if the reference can not be resolved, warn the user and continue. else: console_write('{} {}: {}'.format( Defaults().warning_icon, Defaults().warning_colour( 'WARNING (Mounting)'), Defaults().info_colour( 'The reference "{}" in resource {} is ambigious. It will not be mounted.' .format(ref, resource)))) continue elif key not in ref_list: ref_list[key] = self._find_resource_from_name(ref) else: ref_list[key].extend( self._find_resource_from_name(ref)) # This is where we synchronise constant_value in the configuration section with the resource # for filling up the missing elements that hasn't been defined in the resource due to provider # implementation. target_resource = [ t for t in [self.resources.get(resource, {}).get('address')] if t is not None ] if not target_resource: target_resource = [ k for k in self.resources.keys() if k.startswith(resource) ] for t_r in target_resource: if type(value) is type( self.resources[t_r]['values'].get(key) ) and self.resources[t_r]['values'].get(key) != value: if isinstance(value, (list, dict)): merge_dicts(self.resources[t_r]['values'][key], value) if ref_list: ref_type = self.configuration['resources'][resource][ 'expressions'].get('type', {}) if 'references' in ref_type: ref_type = resource.split('.')[0] if not ref_type and not self.is_type(resource, 'data'): ref_type = self.extract_resource_type_from_address( resource) for k, v in ref_list.items(): v = flatten_list(v) # Mounting A->B source_resources = self._find_resource_from_name( self.configuration['resources'][resource]['address']) self._mount_resources(source=source_resources, target=ref_list, ref_type=ref_type) # Mounting B->A for parameter, target_resources in ref_list.items(): for target_resource in target_resources: if not self.is_type( resource, 'data') and not self.is_type( resource, 'var') and not self.is_type( resource, 'provider'): ref_type = self.extract_resource_type_from_address( target_resource) self._mount_resources( source=[target_resource], target={parameter: source_resources}, ref_type=ref_type)
def _mount_resources(self, source, target, ref_type): ''' Mounts values of the source resource to the target resource's values with ref_type key :param source: source resource :param target: target resource :param ref_type: reference type (e.g. ingress ) :return: none ''' for source_resource in source: if 'values' not in self.resources.get(source_resource, {}): continue for parameter, target_resources in target.items(): for target_resource in target_resources: if target_resource not in self.resources or 'values' not in self.resources[ target_resource]: continue resource = self.resources_raw[source_resource]['values'] # This is a very stupid terraform-provider bug. Somehow, sometimes it loses the state # and sets the value to None - which is normally not allowed.. It should have been an empty # dict instead. Hence, we are fixing that here. if resource is None: defaults = Defaults() console_write('{} {}: {}'.format( defaults.warning_icon, defaults.warning_colour('WARNING (mounting)'), defaults.info_colour( 'The resource "{}" has no values set. This is a terraform provider ' 'bug. Its recommended to remove/fix this resource within your state.' .format(source_resource)))) self.resources_raw[source_resource]['values'] = {} self.resources[source_resource]['values'] = {} resource = {} resource[Defaults.mounted_ptr] = True if Defaults.r_mount_ptr not in self.resources[ target_resource]: self.resources[target_resource][ Defaults.r_mount_ptr] = {} if Defaults.r_mount_addr_ptr not in self.resources[ target_resource]: self.resources[target_resource][ Defaults.r_mount_addr_ptr] = {} if Defaults.r_mount_addr_ptr_list not in self.resources[ target_resource]: self.resources[target_resource][ Defaults.r_mount_addr_ptr_list] = [] # ensure resources[target_resource]['values'] is an # empty dict and not None if not self.resources[target_resource]['values']: self.resources[target_resource]['values'] = dict() if ref_type not in self.resources[target_resource][ 'values']: self.resources[target_resource]['values'][ ref_type] = [] self.resources[target_resource]['values'][ref_type].append( resource) self.resources[target_resource][ Defaults.r_mount_ptr][parameter] = ref_type self.resources[target_resource][ Defaults.r_mount_addr_ptr][parameter] = source target_set = set(self.resources[target_resource][ Defaults.r_mount_addr_ptr_list]) source_set = set(source) self.resources[target_resource][ Defaults.r_mount_addr_ptr_list] = list(target_set | source_set) if parameter not in self.resources[source_resource][ 'values']: self.resources[source_resource]['values'][ parameter] = target_resource
def it_contains_something_old(_step_obj, something, inherited_values=Null): console_write("\t{} {}: {}".format( Defaults().warning_icon, Defaults().warning_colour('WARNING'), Defaults().info_colour( '"When it contains {}" step functionality will be changed' ' on future versions and the functionality will be same ' 'as "When it has {}" step. Please use the ' 'latter.'.format(something, something)))) match = _step_obj.context.match seek_key_in_dict = match.seek_key_in_dict seek_regex_key_in_dict_values = match.seek_regex_key_in_dict_values prop_list = [] _step_obj.context.stash = inherited_values if inherited_values is not Null else _step_obj.context.stash if _step_obj.context.type in ('resource', 'data'): for resource in _step_obj.context.stash: if not isinstance(resource, dict) \ or 'values' not in resource \ or 'address' not in resource \ or 'type' not in resource: resource = { 'values': resource, 'address': resource, 'type': _step_obj.context.name } values = resource.get('values', resource.get('expressions', {})) if not values: values = seek_key_in_dict(resource, something) found_value = Null found_key = Null if isinstance(values, dict): found_key = match.get(values, something, seek_key_in_dict(values, something)) if not isinstance(found_key, list): found_key = [{something: found_key}] if len(found_key): found_key = found_key[0] if len( found_key) == 1 and match.contains( found_key[0], something) else found_key if isinstance(found_key, dict): found_value = match.get(found_key, something, found_key) else: found_value = found_key elif isinstance(values, list): found_value = [] for value in values: if isinstance(value, dict): # First search in the keys found_key = seek_key_in_dict(value, something) # Then search in the values with 'key' if not found_key: found_key = seek_regex_key_in_dict_values( value, 'key', something) if found_key: found_key = found_key[0] found_value = value.get('value') break elif isinstance(value, list): found_key, found_value = it_contains_something_old( _step_obj, something, value) if found_key is not Null and len(found_key): found_key = found_key[0] if len( found_key) == 1 else found_key if isinstance(found_key, dict): found_value.append( match.get(found_key, something, found_key)) if isinstance(found_value, dict) and 'constant_value' in found_value: found_value = found_value['constant_value'] if found_value is not Null and found_value != [] and found_value != '' and found_value != {}: prop_list.append({ 'address': resource['address'], 'values': found_value, 'type': _step_obj.context.name }) if prop_list: _step_obj.context.stash = prop_list _step_obj.context.property_name = something return something, prop_list if _step_obj.state != Step.State.FAILED: skip_step( _step_obj, resource=_step_obj.context.name, message='Can not find any {} property for {} resource in ' 'terraform plan.'.format(something, _step_obj.context.name)) elif _step_obj.context.type == 'provider': for provider_data in _step_obj.context.stash: values = seek_key_in_dict(provider_data, something) if values: _step_obj.context.stash = values _step_obj.context.property_name = something _step_obj.context.address = '{}.{}'.format( provider_data.get('name', _step_obj.context.addresses), provider_data.get('alias', "\b")) return True if _step_obj.state != Step.State.FAILED: skip_step( _step_obj, resource=_step_obj.context.name, message='Skipping the step since {} type does not have {} property.' .format(_step_obj.context.type, something))
def cli(arghandling=ArgHandling(), argparser=ArgumentParser(prog=__app_name__, description='BDD Test Framework for Hashicorp terraform')): args = arghandling parser = argparser parser.add_argument('--terraform', '-t', dest='terraform_file', metavar='terraform_file', type=str, nargs='?', help='The absolute path to the terraform executable.', required=False) parser.add_argument('--features', '-f', dest='features', metavar='feature directory', action=ReadableDir, help='Directory (or git repository with "git:" prefix) consists of BDD features', required=True) parser.add_argument('--planfile', '-p', dest='plan_file', metavar='plan_file', action=ReadablePlan, help='Plan output file generated by Terraform', required=True) parser.add_argument('--quit-early', '-q', dest='exit_on_failure', action='store_true', help='Stops executing any more steps in a scenario on first failure.', required=False) parser.add_argument('--no-failure', '-n', dest='no_failure', action='store_true', help='Skip all the tests that is failed, but giving proper failure message', required=False) parser.add_argument('--silent', '-S', dest='silence', action='store_true', help='Do not output any scenarios, just write results or failures', required=False) parser.add_argument('--identity', '-i', dest='ssh_key', metavar='ssh private key', type=str, nargs='?', help='SSH Private key that will be use on git authentication.', required=False) parser.add_argument('--version', '-v', action='version', version=__version__) _, radish_arguments = parser.parse_known_args(namespace=args) steps_directory = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'steps') # SSH Key is given for git authentication ssh_cmd = {} if args.ssh_key: ssh_cmd = {'GIT_SSH_COMMAND': 'ssh -l {} -i {}'.format('git', args.ssh_key)} features_dir = '/' # A remote repository used here if args.features.startswith(('http', 'https', 'ssh')): # Default to master branch and full repository if args.features.endswith('.git'): features_git_repo = args.features features_git_branch = 'master' # Optionally allow for directory and branch elif '.git//' in args.features and '?ref=' in args.features: # Split on .git/ features_git_list = args.features.split('.git/', 1) # Everything up to .git is the repository features_git_repo = features_git_list[0] + '.git' # Split the directory and branch ref features_git_list = features_git_list[1].split('?ref=', 1) features_dir = features_git_list[0] features_git_branch = features_git_list[1] else: # invalid raise ValueError("Bad feature directory:" + args.features) # Clone repository args.features = mkdtemp() Repo.clone_from(url=features_git_repo, to_path=args.features, env=ssh_cmd, depth=1, branch=features_git_branch) features_directory = os.path.join(os.path.abspath(args.features) + features_dir) commands = ['radish', '--write-steps-once', features_directory, '--basedir', steps_directory, '--user-data=plan_file={}'.format(args.plan_file), '--user-data=exit_on_failure={}'.format(args.exit_on_failure), '--user-data=terraform_executable={}'.format(args.terraform_file), '--user-data=no_failure={}'.format(args.no_failure), '--user-data=silence_mode_enabled={}'.format(args.silence)] commands.extend(radish_arguments) console_write('{} {} {}{}'.format(Defaults().icon, Defaults().yellow('Features\t:'), features_directory, (' ({})'.format(features_git_repo) if 'features_git_repo' in locals() else ''))) console_write('{} {} {}'.format(Defaults().icon, Defaults().green('Plan File\t:'), args.plan_file)) if args.silence is True: console_write('{} Suppressing output enabled.'.format(Defaults().icon)) commands.append('--formatter=dotter') if args.exit_on_failure is True: console_write('{} {}\t\t: Scenario executions will stop on first step {}.'.format(Defaults().info_icon, Defaults().info_colour('INFO'), Defaults().failure_colour('failure'))) if args.no_failure is True: console_write('{} {}\t: {}ping all {} steps, exit code will always be {}.'.format(Defaults().warning_icon, Defaults().warning_colour('WARNING'), Defaults().skip_colour('SKIP'), Defaults().failure_colour('failure'), Defaults().info_colour(0))) if Defaults().interactive_mode is False: console_write('{} Running in non-interactive mode.'.format(Defaults().info_icon)) if args.silence is False: console_write('\n{} Running tests. {}\n'.format(Defaults().icon, Defaults().tada)) try: result = call_radish(args=commands[1:]) except IndexError as e: print(e) return result
# # This is used to override some stuff that does not fit our needs within radish-bdd. # Better not to touch anything here. from terraform_compliance.extensions.override_radish_step import Step as StepOverride from radish.stepmodel import Step Step.run = StepOverride.run from terraform_compliance.extensions.override_radish_hookerrors import handle_exception as handle_exception_override from radish import errororacle errororacle.handle_exception = handle_exception_override ## # if __version__ == "{{VERSION}}": __version__ = "\blocal development version" print('{} v{} initiated\n'.format(__app_name__, Defaults().yellow(__version__))) class ArgHandling(object): pass def cli(arghandling=ArgHandling(), argparser=ArgumentParser(prog=__app_name__, description='BDD Test Framework for Hashicorp terraform')): args = arghandling parser = argparser parser.add_argument('--terraform', '-t', dest='terraform_file', metavar='terraform_file', type=str, nargs='?', help='The absolute path to the terraform executable.', required=False) parser.add_argument('--features', '-f', dest='features', metavar='feature directory', action=ReadableDir, help='Directory (or git repository with "git:" prefix) consists of BDD features', required=True) parser.add_argument('--planfile', '-p', dest='plan_file', metavar='plan_file', action=ReadablePlan,
# This is used to override some stuff that does not fit our needs within radish-bdd. # Better not to touch anything here. from terraform_compliance.extensions.override_radish_step import Step as StepOverride from radish.stepmodel import Step Step.run = StepOverride.run from terraform_compliance.extensions.override_radish_hookerrors import handle_exception as handle_exception_override from radish import errororacle errororacle.handle_exception = handle_exception_override ## # if __version__ == "{{VERSION}}": __version__ = "\blocal development version" print('{} v{} initiated\n'.format(__app_name__, Defaults().yellow(__version__))) class ArgHandling(object): pass def cli(arghandling=ArgHandling(), argparser=ArgumentParser( prog=__app_name__, description='BDD Test Framework for Hashicorp terraform')): args = arghandling parser = argparser parser.add_argument('--terraform', '-t', dest='terraform_file',
def skip_step(step, resource=None, message=None): if resource is None: resource = 'any' if message is None: message = '{} {} {}'.format(Defaults().yellow('Can not find'), Defaults().green(resource), Defaults().yellow('defined in target terraform plan.')) e_message = 'Can not find {} defined in target terraform plan.'.format(resource) else: e_message = message message = Defaults().yellow(message) if step.context.no_skip: if -1 in step.context.lines_to_noskip or step.line in step.context.lines_to_noskip: message = Defaults().failure_colour(message) Error(step, e_message) return if str(world.config.formatter) in ('gherkin'): console_write("\t{} {}: {}".format(Defaults().info_icon, Defaults().skip_colour('SKIPPING'), message.format(resource=Defaults().green(resource))) ) step.skip() # Skip all steps in the scenario for each in step.parent.all_steps: each.runable = False
def write_failure(failure): """ Writes the failure to the console """ console_write("\n{0}".format(Defaults().failure_colour(failure.traceback)))