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))
Exemple #9
0
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
Exemple #10
0
#
# 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,
Exemple #11
0
# 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',
Exemple #12
0
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)))