Skip to content

Commit

Permalink
Merge pull request #25 from eerkunt/coverage_and_locals
Browse files Browse the repository at this point in the history
Coverage and locals
  • Loading branch information
eerkunt authored Sep 9, 2018
2 parents 26f9813 + a4c1d96 commit c4dab19
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 96 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
venv
*,cover
coverage.xml
.coverage
.scannerwork
.pytest_cache
*.pyc
Expand Down
13 changes: 11 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ services:
- docker
cache:
pip: true
install:
- pip install coveralls
- pip install -r requirements.txt
after_success: coveralls
jobs:
include:
- stage: Tests
provider: script
script: py.test -v
script:
- py.test -v
- coverage run --source terraform_compliance setup.py test

- stage: Code Quality
addons:
Expand All @@ -18,7 +24,10 @@ jobs:
token: $SONAR_LOGIN
script:
- "echo 'Scanning source code with SonarCloud'"
- sonar-scanner -Dsonar.projectKey=terraform-compliance -Dsonar.organization=eerkunt-github -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_LOGIN
- coverage erase
- coverage run --branch --source=terraform_compliance setup.py test
- coverage xml -i
- sonar-scanner -Dsonar.projectKey=terraform-compliance -Dsonar.organization=eerkunt-github -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_LOGIN -Dsonar.python.coverage.reportPath=coverage.xml

- stage: Build & Deploy (PYPI)
script: "echo 'PYPI Build & Deploy'"
Expand Down
20 changes: 20 additions & 0 deletions example/example_01/aws/naming_standards.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: Resources should have a proper naming standard
In order to keep consistency between resources
As engineers
We'll enforce naming standards

Scenario Outline: Naming Standard on all available resources
Given I have <resource_name> defined
Then it should contain <name_key>
And its value must match the "\${var.project}-\${var.environment}-\${var.application}-.*" regex

Examples:
| resource_name | name_key |
| AWS EC2 instance | name |
| AWS ELB resource | name |
| AWS RDS instance | name |
| AWS S3 Bucket | name |
| AWS EBS volume | name |
| AWS Auto-Scaling Group | name |
| aws_key_pair | key_name |
| aws_ecs_cluster | name |
2 changes: 1 addition & 1 deletion terraform_compliance/common/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from netaddr import IPNetwork


# A helper function that will be used to flattan a multi-dimensional multi-nested list
# A helper function that will be used to flatten a multi-dimensional multi-nested list
def flatten_list(input):
new_list = []
for i in input:
Expand Down
45 changes: 45 additions & 0 deletions terraform_compliance/common/pyhcl_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from sys import exc_info, exit
from os.path import isdir
from terraform_compliance import Validator
from terraform_validate.terraform_validate import TerraformSyntaxException


def load_tf_files(tf_directory):
result = False
print('Reading terraform files.')

if isdir('{}/.terraform'.format(tf_directory)):
print('ERROR: You already have a .terraform directory within your terraform files.')
print(' This will lead to run tests against those imported modules. Please delete the directory to continue.')
exit(2)

while result is False:
try:
Validator(tf_directory)
print('All HCL files look good.')
result = True

except ValueError:
print('Unable to validate Terraform Files.')
print('ERROR: {}'.format(exc_info()[1]))
exit(1)
except TerraformSyntaxException:
pad_invalid_tf_files(exc_info()[1])

return result


def pad_invalid_tf_files(exception_message):
exception_message = str(exception_message).split('\n')
if 'Unexpected end of file' in exception_message[1]:
filename = exception_message[0].split(' ')[-1:][0]
print('Invalid HCL file: {}. Fixing it.'.format(filename))
pad_tf_file(filename)
return True

return False


def pad_tf_file(file):
with open(file, 'a') as f:
f.write('variable {}')
35 changes: 35 additions & 0 deletions terraform_compliance/common/readable_dir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import sys
import os
from argparse import Action


class ReadableDir(Action):
def __init__(self, dest, required, help, option_strings=None, metavar=None):
super(ReadableDir, self).__init__(dest, required, help, option_strings, metavar)
self.dest = dest
self.required = required
self.help = help
self.option_strings = option_strings
self.metavar = metavar

def __call__(self, parser, namespace, values, option_string=None):
prospective_dir = values

# Check if the given directory is actually a git repo
if prospective_dir.startswith('git:'):
print('Using remote git repository: {}'.format(prospective_dir[4:]))
setattr(namespace, self.dest, prospective_dir[4:])
return True

# Check if the given path is a directory really
if not os.path.isdir(prospective_dir):
print('ERROR: {} is not a directory.'.format(prospective_dir))
sys.exit(1)

# Check if we have access to that directory
if os.access(prospective_dir, os.R_OK):
setattr(namespace, self.dest, prospective_dir)
return True

print('ERROR: Can not read {}'.format(prospective_dir))
sys.exit(1)
72 changes: 26 additions & 46 deletions terraform_compliance/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import sys
import os
from argparse import ArgumentParser, Action
from terraform_compliance import Validator
from argparse import ArgumentParser
from radish.main import main as call_radish
from tempfile import mkdtemp
from git import Repo
from terraform_compliance.common.pyhcl_helper import load_tf_files
from distutils.dir_util import copy_tree
from shutil import rmtree
from terraform_compliance.common.readable_dir import ReadableDir


__app_name__ = "terraform-compliance"
__version__ = "0.3.6"
__version__ = "0.3.7"


class ArgHandling(object):
Expand All @@ -16,30 +19,6 @@ class ArgHandling(object):
#TODO: Handle all directory/protocol handling via a better class structure here.
#TODO: Extend git: (on features or tf files argument) into native URLs instead of using a prefix here.

class ReadableDir(Action):
def __call__(self, parser, namespace, values, option_string=None):
prospective_dir = values

# Check if the given directory is actually a git repo
if prospective_dir.startswith('git:'):
print('Using remote git repository: {}'.format(prospective_dir[4:]))
setattr(namespace, self.dest, prospective_dir[4:])
return True

# Check if the given path is a directory really
if not os.path.isdir(prospective_dir):
print('ERROR: {} is not a directory.'.format(prospective_dir))
sys.exit(1)

# Check if we have access to that directory
if os.access(prospective_dir, os.R_OK):
setattr(namespace, self.dest, prospective_dir)
return True

print('ERROR: Can not read {}'.format(prospective_dir))
sys.exit(1)


def cli():
argument = ArgHandling()
parser = ArgumentParser(prog=__app_name__,
Expand All @@ -57,42 +36,43 @@ def cli():
steps_directory = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'steps')
print('Steps : {}'.format(steps_directory))

# A remote repository used here
if argument.features.startswith('http'):
features_git_repo = argument.features
argument.features = mkdtemp()
Repo.clone_from(features_git_repo, argument.features)
features_directory = os.path.join(os.path.abspath(argument.features))
print('Features : {}{}'.format(features_directory, (' ({})'.format(features_git_repo) if 'features_git_repo' in locals() else '')))

tf_tmp_dir = mkdtemp()

# A remote repository is used here.
if argument.tf_dir.startswith('http'):
tf_git_repo = argument.tf_dir
argument.tf_dir = mkdtemp()
Repo.clone_from(tf_git_repo, argument.tf_dir)
tf_directory = os.path.join(os.path.abspath(argument.tf_dir))
print('TF Files : {}{}'.format(tf_directory, (' ({})'.format(tf_git_repo) if 'tf_git_repo' in locals() else '')))
Repo.clone_from(tf_git_repo, tf_tmp_dir)

# A local directory is used here
else:
# Copy the given local directory to another place, since we may change some tf files for compatibility.
copy_tree(argument.tf_dir, tf_tmp_dir)

tf_directory = os.path.join(os.path.abspath(tf_tmp_dir))
print('TF Files : {} ({})'.format(tf_directory, argument.tf_dir))

commands = ['radish',
features_directory,
'--basedir', steps_directory,
'--user-data=tf_dir={}'.format(tf_directory)]
commands.extend(radish_arguments)

try:
print('Validating terraform files.')
Validator(tf_directory)
print('All HCL files look good.')

except ValueError:
print('Unable to validate Terraform Files.')
print('ERROR: {}'.format(sys.exc_info()[1]))
sys.exit(1)

load_tf_files(tf_directory)
print('Running tests.')
return call_radish(args=commands[1:])
result = call_radish(args=commands[1:])

# Delete temporary directory we created
print('Cleaning up.')
rmtree(tf_directory)


if __name__ == '__main__':
cli()

#TODO: Implement a cleanup for temporary directories since they are not deleted.
#TODO: If .terraform directory exist in '.' the just exit with a different exit code.
48 changes: 24 additions & 24 deletions terraform_compliance/steps/steps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-

from radish import step, world, custom_type, then, when, given
from terraform_compliance.steps import untaggable_resources, regex, resource_name, encryption_property
from radish import step, world, custom_type, given
from terraform_compliance.steps import resource_name, encryption_property
from terraform_compliance.common.helper import check_sg_rules
from terraform_compliance.extensions.terraform_validate import normalise_tag_values

Expand All @@ -13,30 +13,29 @@ def arg_exp_for_secure_text(text):

@given(u'I have {resource:ANY} defined')
def define_a_resource(step, resource):
world.config.terraform.error_if_property_missing()
if (resource in resource_name.keys()):
resource = resource_name[resource]

step.context.resource_type = resource
step.context.stash = step.context.resources = world.config.terraform.resources(resource)
step.context.stash = world.config.terraform.resources(resource)


@step(u'I {action_type:ANY} them')
def i_action_them(step, action_type):
if not step.context.resources.resource_list:
if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list:
return

if action_type == "count":
step.context.stash = len(step.context.stash.resource_list)
elif action_type == "sum":
step.context.stash = sum(step.context.stash.resource_list)
else:
AssertionError("Invalid action_type in the scenario: {}".format(action))
AssertionError("Invalid action_type in the scenario: {}".format(action_type))


@step(u'I expect the result is {operator:ANY} than {number:d}')
def func(step, operator, number):
if not step.context.resources.resource_list:
def i_expect_the_result_is(step, operator, number):
if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list:
return

value = int(step.context.stash)
Expand All @@ -54,58 +53,59 @@ def func(step, operator, number):


@step(u'it {condition:ANY} contain {something:ANY}')
def func(step, condition, something):
if not step.context.resources.resource_list:
def it_contain(step, condition, something):
if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list:
return

if condition == 'must':
world.config.terraform.error_if_property_missing()
step.context.stash.should_have_properties(something)

if something in resource_name.keys():
something = resource_name[something]

step.context.resource_type = something
step.context.resources = step.context.resources.property(something)
if type(something) not in [str, unicode]:
step.context.resource_type = something

step.context.stash = step.context.stash.property(something)

if condition == 'must':
assert step.context.resources.properties
assert step.context.stash.properties


@step(u'encryption is enabled')
@step(u'encryption must be enabled')
def func(step):
if not step.context.resources.resource_list:
def encryption_is_enabled(step):
if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list:
return

world.config.terraform.error_if_property_missing()
prop = encryption_property[step.context.resource_type]
step.context.resources.property(prop).should_equal(True)
step.context.stash.property(prop).should_equal(True)


@step(u'its value must match the "{regex_type}" regex')
def func(step, regex_type):
if not step.context.resources.resource_list:
if hasattr(step.context.stash, 'resource_list') or not step.context.stash.resource_list:
return

normalise_tag_values(step.context.properties)
step.context.properties.property(regex_type).should_match_regex(step.context.regex)


@step(u'its value must be set by a variable')
def func(step):
if not step.context.resources.resource_list:
def its_value_must_be_set_by_a_variable(step):
if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list:
return

step.context.resources.property(step.context.search_value).should_match_regex('\${var.(.*)}')
step.context.stash.property(step.context.search_value).should_match_regex(r'\${var.(.*)}')


@step(u'it must not have {proto} protocol and port {port:d} for {cidr:ANY}')
def func(step, proto, port, cidr):
def it_must_not_have_sg_stuff(step, proto, port, cidr):
proto = str(proto)
port = int(port)
cidr = str(cidr)

for item in step.context.resources.properties:
for item in step.context.stash.properties:
if type(item.property_value) is list:
for security_group in item.property_value:
check_sg_rules(world.config.terraform.terraform_config, security_group, proto, port, cidr)
Expand Down
Loading

0 comments on commit c4dab19

Please sign in to comment.