def _process(self): # Prepare message msg = [] for msg_index in range(0,len(self.message)): if self.exit_on_failure is False: msg_header = '{}{}'.format(self.exception_name, colorful.bold_white(':')) if msg_index == 0 else ' '*(len(self.exception_name)+1) msg.append('\t\t{} {}'.format(colorful.bold_red(msg_header), colorful.red(self.message[msg_index]))) else: msg.append(self.message[msg_index] if msg_index == 0 else '{}{} {} {}'.format("\t"*2, ' '*(len(self.exception_name)+1), colorful.bold_white(':'), self.message[msg_index])) if self.exit_on_failure is False: for message in msg: console_write(message) if self.no_failure is False: self._fail_step(self.step_obj.id) else: self.step_obj.state = Step.State.SKIPPED return if self.no_failure is False: raise self.exception('\n'.join(msg))
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_error(text): """ Writes the given text to the console """ console_write("{} {}: {}".format(Defaults().warning_icon, Defaults().failure_colour("ERROR"), Defaults().warning_colour(text)))
def write_stdout(level, message): prefix = colorful.bold_yellow(u'\u229b INFO :') if level == 'WARNING': prefix = colorful.bold_red(u'\u2757 WARNING :') message = colorful.yellow(message) added_prefix = u'\n\t\t{}\t{} '.format(colorful.gray(u'\u2502'),' '*len(prefix)) message = message.split('\n') console_write(u'\t\t\u251c\u2501\t{} {}'.format(prefix, added_prefix.join(message)))
def python_version_check(): python_version = sys.version.split(' ')[0] if not python_version: raise TerraformComplianceInternalFailure('Could not determine python version. ' 'Please post this to issues: '.format(sys.version)) python_version = VersionInfo.parse(python_version) if compare(str(python_version), Defaults.supported_min_python_versions) < 0: console_write('ERROR: Python version {} is not supported. ' 'You must have minimum {} version.'.format(python_version, Defaults.supported_min_python_versions[0])) sys.exit(1) return True
def _process(self): # Prepare message msg = [] for msg_index in range(0, len(self.message)): if self.exit_on_failure is False or self.no_failure is True: msg_header = '{}{}'.format( self.exception_name, colorful.bold_white(':') ) if msg_index == 0 else ' ' * (len(self.exception_name) + 1) if str(world.config.formatter) in ('gherkin'): # this line could be improved by letting radish handle the printing msg.append('\t\t{} {}'.format( colorful.bold_red(msg_header), colorful.red(self.message[msg_index]))) elif str(world.config.formatter) in ('silent_formatter'): msg.append('{} '.format(colorful.bold_red(msg_header))) msg.append('{}'.format( colorful.red(self.message[msg_index]))) else: msg.append( self.message[msg_index] if msg_index == 0 else '{}{} {} {}' .format("\t" * 2, ' ' * (len(self.exception_name) + 1), colorful.bold_white(':'), self.message[msg_index])) if self.exit_on_failure is False or (self.no_failure is True and msg): if str(world.config.formatter) in ('gherkin'): for message in msg: console_write(message) elif str(world.config.formatter) in ('silent_formatter'): if not hasattr(self.step_obj.context, 'failure_msg'): # where to put this self.step_obj.context.failure_msg = [] self.step_obj.context.failure_msg.extend(msg) if self.no_failure is False: self._fail_step(self.step_obj.id) else: self.step_obj.state = Step.State.SKIPPED for step in self.step_obj.parent.all_steps: step.runable = False return if self.no_failure is False: raise self.exception('\n'.join(msg))
def silent_formatter_after_each_feature(self, feature): """ Writes failing features to the console :param Feature feature: the feature to write to the console """ if not any(scenario.state == 'failed' for scenario in feature.all_scenarios): return # ConsoleWriter.console_writer_before_each_feature(self, feature) self.console_writer_before_each_feature(feature) for scenario in feature.all_scenarios: self.silent_formatter_after_each_scenario(scenario) # one newline between final scenario and results summary console_write('')
def skip_step(step, resource=None, message=None): if resource is None: resource = 'any' if message is None: message = '{} {} {}'.format( colorful.orange('Can not find'), colorful.magenta(resource), colorful.orange('defined in target terraform plan.')) else: message = colorful.orange(message) console_write("\t{}: {}".format( colorful.bold_purple('SKIPPING'), message.format(resource=colorful.magenta(resource)))) step.skip() # Skip all steps in the scenario for each in step.parent.all_steps: each.runable = False
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 handle_exception(exception): """ Handle the given exception This will print more information about the given exception :param Exception exception: the exception to handle """ if isinstance(exception, HookError): write_error(exception) write_failure(exception.failure) abort(1) elif isinstance(exception, RadishError): write_error(handle_radish_errors(exception)) abort(1) elif isinstance(exception, KeyboardInterrupt): console_write("Aborted by the user...") abort(1) else: write_error(exception) write_failure(Failure(exception)) abort(2)
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
def write_failure(failure): """ Writes the failure to the console """ console_write("\n{0}".format(Defaults().failure_colour(failure.traceback)))