Merge pull request #4 from beelit94/python-terraform-3

fixed #3
This commit is contained in:
beelit94 2017-01-04 00:30:31 -06:00 committed by GitHub
commit 896cc822bc
9 changed files with 130 additions and 163 deletions

View file

@ -30,6 +30,12 @@ script:
- "export PATH=$PATH:$PWD/tf_bin" - "export PATH=$PATH:$PWD/tf_bin"
- tox - tox
branches:
only:
- master
- develop
- release/**
deploy: deploy:
# test pypi # test pypi
- provider: pypi - provider: pypi

View file

@ -1 +0,0 @@
include VERSION

View file

@ -9,15 +9,6 @@ python-terraform is a python module provide a wrapper of `terraform` command lin
## Installation ## Installation
pip install python-terraform pip install python-terraform
## Implementation
IMHO, how terraform design boolean options is confusing.
Take `input=True` and `-no-color` option of `apply` command for example,
they're all boolean value but with different option type.
This make api caller don't have a general rule to follow but to do
a exhaustive method implementation which I don't prefer to.
Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option
like `-no-color` and `True/False` value reserved for option like
## Usage ## Usage
####For any terraform command ####For any terraform command
@ -25,10 +16,13 @@ like `-no-color` and `True/False` value reserved for option like
t = Terraform() t = Terraform()
return_code, stdout, stderr = t.<cmd_name>(*arguments, **options) return_code, stdout, stderr = t.<cmd_name>(*arguments, **options)
####For any parameter ####For any argument
simply pass as argument in order of method, for example, simply pass the string to arguments of the method, for example,
terraform apply target_dir --> <instance>.apply('target_dir') terraform apply target_dir
--> <instance>.apply('target_dir')
terraform import aws_instance.foo i-abcd1234
--> <instance>.import('aws_instance.foo', 'i-abcd1234')
####For any options ####For any options
@ -86,6 +80,12 @@ or
tf = Terraform() tf = Terraform()
tf.apply('/home/test', no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'}) tf.apply('/home/test', no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'})
or
from python_terraform import Terraform
tf = Terraform(working_dir='/home/test', variables={'a':'b', 'c':'d'})
tf.apply(no_color=IsFlagged, refresh=False)
#### 2. fmt command, diff=true #### 2. fmt command, diff=true
In shell: In shell:
@ -98,6 +98,25 @@ In python-terraform:
tf = terraform(working_dir='/home/test') tf = terraform(working_dir='/home/test')
tf.fmt(diff=True) tf.fmt(diff=True)
## default values
for apply/plan/destroy command, assign with following default value to make
caller easier in python
1. ```input=False```, in this case process won't hang because you missing a variable
1. ```no_color=IsFlagged```, in this case, stdout of result is easier for parsing
## Implementation
IMHO, how terraform design boolean options is confusing.
Take `input=True` and `-no-color` option of `apply` command for example,
they're all boolean value but with different option type.
This make api caller don't have a general rule to follow but to do
a exhaustive method implementation which I don't prefer to.
Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option
like `-no-color` and `True/False` value reserved for option like `refresh=true`

View file

@ -1 +0,0 @@
0.8.1

View file

@ -34,13 +34,17 @@ class Terraform(object):
var_file=None, var_file=None,
terraform_bin_path=None): terraform_bin_path=None):
""" """
:param working_dir: the folder of the working folder, if not given, will be where python :param working_dir: the folder of the working folder, if not given,
will be current working folder
:param targets: list of target :param targets: list of target
:param state: path of state file relative to working folder as default value of apply/destroy/plan command
:param variables: variables for apply/destroy/plan command :param state: path of state file relative to working folder,
:param parallelism: parallelism for apply/destroy command as a default value of apply/destroy/plan command
:param var_file: passed as value of -var-file option, could be string or list :param variables: default variables for apply/destroy/plan command,
list stands for multiple -var-file option will be override by variable passing by apply/destroy/plan method
:param parallelism: default parallelism value for apply/destroy command
:param var_file: passed as value of -var-file option,
could be string or list, list stands for multiple -var-file option
:param terraform_bin_path: binary path of terraform :param terraform_bin_path: binary path of terraform
""" """
self.working_dir = working_dir self.working_dir = working_dir
@ -64,21 +68,28 @@ class Terraform(object):
return wrapper return wrapper
def apply(self, dir_or_plan=None, **kwargs): def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged, **kwargs):
""" """
refer to https://terraform.io/docs/commands/apply.html refer to https://terraform.io/docs/commands/apply.html
no-color is flagged by default no-color is flagged by default
:param no_color: disable color of stdout
:param input: disable prompt for a missing variable
:param dir_or_plan: folder relative to working folder :param dir_or_plan: folder relative to working folder
:param kwargs: same as kwags in method 'cmd' :param kwargs: same as kwags in method 'cmd'
:returns return_code, stdout, stderr :returns return_code, stdout, stderr
""" """
default = dict() default = kwargs
args, option_dict = self._create_cmd_args(dir_or_plan, default, kwargs) default['input'] = input
default['no_color'] = no_color
option_dict = self._generate_default_options(default)
args = self._generate_default_args(dir_or_plan)
return self.cmd('apply', *args, **option_dict) return self.cmd('apply', *args, **option_dict)
def _create_cmd_args(self, dir_or_plan, default_dict, kwargs): def _generate_default_args(self, dir_or_plan):
option_dict = default_dict return [dir_or_plan] if dir_or_plan else []
def _generate_default_options(self, input_options):
option_dict = dict()
option_dict['state'] = self.state option_dict['state'] = self.state
option_dict['target'] = self.targets option_dict['target'] = self.targets
option_dict['var'] = self.variables option_dict['var'] = self.variables
@ -86,19 +97,34 @@ class Terraform(object):
option_dict['parallelism'] = self.parallelism option_dict['parallelism'] = self.parallelism
option_dict['no_color'] = IsFlagged option_dict['no_color'] = IsFlagged
option_dict['input'] = False option_dict['input'] = False
option_dict.update(kwargs) option_dict.update(input_options)
args = [dir_or_plan] if dir_or_plan else [] return option_dict
return args, option_dict
def destroy(self, dir_or_plan=None, **kwargs): def destroy(self, dir_or_plan=None, force=IsFlagged, **kwargs):
""" """
refer to https://www.terraform.io/docs/commands/destroy.html refer to https://www.terraform.io/docs/commands/destroy.html
force/no-color option is flagged by default force/no-color option is flagged by default
:return: ret_code, stdout, stderr :return: ret_code, stdout, stderr
""" """
default = {'force': IsFlagged} default = kwargs
args, option_dict = self._create_cmd_args(dir_or_plan, default, kwargs) default['force'] = force
return self.cmd('destroy', *args, **option_dict) options = self._generate_default_options(default)
args = self._generate_default_args(dir_or_plan)
return self.cmd('destroy', *args, **options)
def plan(self, dir_or_plan=None, detailed_exitcode=IsFlagged, **kwargs):
"""
refert to https://www.terraform.io/docs/commands/plan.html
:param detailed_exitcode: Return a detailed exit code when the command exits.
:param dir_or_plan: relative path to plan/folder
:param kwargs: options
:return: ret_code, stdout, stderr
"""
options = kwargs
options['detailed_exitcode'] = detailed_exitcode
options = self._generate_default_options(options)
args = self._generate_default_args(dir_or_plan)
return self.cmd('plan', *args, **options)
def generate_cmd_string(self, cmd, *args, **kwargs): def generate_cmd_string(self, cmd, *args, **kwargs):
""" """
@ -149,15 +175,12 @@ class Terraform(object):
cmds += ['-{k}'.format(k=k)] cmds += ['-{k}'.format(k=k)]
continue continue
if v is IsNotFlagged: if v is None or v is IsNotFlagged:
continue continue
if type(v) is bool: if type(v) is bool:
v = 'true' if v else 'false' v = 'true' if v else 'false'
if not v:
continue
cmds += ['-{k}={v}'.format(k=k, v=v)] cmds += ['-{k}={v}'.format(k=k, v=v)]
cmds += args cmds += args

View file

@ -1,104 +0,0 @@
import subprocess
import click
import os
from distutils.version import StrictVersion
import shutil
import re
def get_version():
p = get_version_file_path()
with open(p) as f:
version = f.read()
version = version.strip()
if not version:
raise ValueError("could not read version")
return version
def write_version(version_tuple):
p = get_version_file_path()
with open(p, 'w+') as f:
f.write('.'.join([str(i) for i in version_tuple]))
def get_version_file_path():
p = os.path.join(os.path.dirname(
os.path.abspath(__file__)), 'VERSION')
return p
def release_patch(version_tuple):
patch_version = version_tuple[2] + 1
new_version = version_tuple[:2] + (patch_version,)
click.echo('new version: %s' % str(new_version))
write_version(new_version)
return new_version
def release_minor(version_tuple):
minor = version_tuple[1] + 1
new_version = version_tuple[:1] + (minor, 0)
click.echo('new version: %s' % str(new_version))
write_version(new_version)
return new_version
def release_major(version_tuple):
major = version_tuple[0] + 1
new = (major, 0, 0)
click.echo('new version: %s' % str(new))
write_version(new)
return new
@click.command()
@click.option('--release', '-r', type=click.Choice(['major', 'minor', 'patch']), default='patch')
@click.option('--url', prompt=True, default=lambda: os.environ.get('FURY_URL', ''))
def main(release, url):
version_tuple = StrictVersion(get_version()).version
click.echo('old version: %s' % str(version_tuple))
if release == 'major':
new_v = release_major(version_tuple)
elif release == 'minor':
new_v = release_minor(version_tuple)
else:
new_v = release_patch(version_tuple)
new_v_s = '.'.join([str(i) for i in new_v])
os.chdir(os.path.dirname(os.path.abspath(__file__)))
subprocess.call("python setup.py sdist", shell=True)
pkg_file = ''
for f in os.listdir('dist'):
r = re.compile(r'.*(%s\.tar\.gz)' % re.escape(new_v_s))
result = r.match(f)
if result:
click.echo(f + ' is ready')
pkg_file = f
break
if not pkg_file:
raise ValueError
this_folder = os.path.dirname(os.path.abspath(__file__))
dist_folder = os.path.join(this_folder, 'dist')
pkg_file_name = pkg_file
pkg_file = os.path.join(dist_folder, pkg_file)
shutil.move(pkg_file, this_folder)
shutil.rmtree(dist_folder)
os.remove('MANIFEST')
is_release = click.confirm('ready to release?')
if is_release:
subprocess.call('curl -F package=@%s %s' % (pkg_file_name, url),
shell=True)
os.remove(pkg_file_name)
if __name__ == '__main__':
main()

View file

@ -2,7 +2,6 @@
This is a python module provide a wrapper of terraform command line tool This is a python module provide a wrapper of terraform command line tool
""" """
from setuptools import setup from setuptools import setup
import os
dependencies = [] dependencies = []
module_name = 'python-terraform' module_name = 'python-terraform'
@ -16,19 +15,9 @@ except IOError:
long_description = short_description long_description = short_description
def get_version():
p = os.path.join(os.path.dirname(
os.path.abspath(__file__)), "VERSION")
with open(p) as f:
version = f.read()
version = version.strip()
if not version:
raise ValueError("could not read version")
return version
setup( setup(
name=module_name, name=module_name,
version=get_version(), version='0.8.1',
url='https://github.com/beelit94/python-terraform', url='https://github.com/beelit94/python-terraform',
license='MIT', license='MIT',
author='Freddy Tan', author='Freddy Tan',

View file

@ -32,7 +32,8 @@ CMD_CASES = [
] ]
] ]
@pytest.fixture()
@pytest.fixture(scope='function')
def fmt_test_file(request): def fmt_test_file(request):
target = os.path.join(current_path, 'bad_fmt', 'test.backup') target = os.path.join(current_path, 'bad_fmt', 'test.backup')
orgin = os.path.join(current_path, 'bad_fmt', 'test.tf') orgin = os.path.join(current_path, 'bad_fmt', 'test.tf')
@ -79,16 +80,18 @@ class TestTerraform(object):
assert ret == 0 assert ret == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
("folder", "variables", "var_files", "expected_output"), ("folder", "variables", "var_files", "expected_output", "options"),
[ [
("var_to_output", {'test_var': 'test'}, None, "test_output=test"), ("var_to_output",
("var_to_output", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]"), {'test_var': 'test'}, None, "test_output=test", {}),
("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, None, "test_map_output={a=ab=bc=cd=d}"), ("var_to_output", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]", {}),
("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, 'var_to_output/test_map_var.json', "test_map_output={a=ab=bc=cd=de=ef=f}") ("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, None, "test_map_output={a=ab=bc=cd=d}", {}),
("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, 'var_to_output/test_map_var.json', "test_map_output={a=ab=bc=cd=de=ef=f}", {}),
("var_to_output", {}, None, "\x1b[0m\x1b[1m\x1b[32mApplycomplete!", {"no_color": IsNotFlagged})
]) ])
def test_apply(self, folder, variables, var_files, expected_output): def test_apply(self, folder, variables, var_files, expected_output, options):
tf = Terraform(working_dir=current_path, variables=variables, var_file=var_files) tf = Terraform(working_dir=current_path, variables=variables, var_file=var_files)
ret, out, err = tf.apply(folder) ret, out, err = tf.apply(folder, **options)
assert ret == 0 assert ret == 0
assert expected_output in out.replace('\n', '').replace(' ', '') assert expected_output in out.replace('\n', '').replace(' ', '')
assert err == '' assert err == ''
@ -129,6 +132,8 @@ class TestTerraform(object):
no_color=IsNotFlagged) no_color=IsNotFlagged)
out = out.replace('\n', '') out = out.replace('\n', '')
assert '\x1b[0m\x1b[1m\x1b[32mApply' in out assert '\x1b[0m\x1b[1m\x1b[32mApply' in out
out = tf.output('test_output')
assert 'test2' in out
def test_get_output(self): def test_get_output(self):
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
@ -141,7 +146,18 @@ class TestTerraform(object):
assert ret == 0 assert ret == 0
assert 'Destroy complete! Resources: 0 destroyed.' in out assert 'Destroy complete! Resources: 0 destroyed.' in out
def test_fmt(self): @pytest.mark.parametrize(
("plan", "variables", "expected_ret"),
[
('vars_require_input', {}, 1)
]
)
def test_plan(self, plan, variables, expected_ret):
tf = Terraform(working_dir=current_path, variables=variables)
ret, out, err = tf.plan(plan)
assert ret == expected_ret
def test_fmt(self, fmt_test_file):
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
ret, out, err = tf.fmt(diff=True) ret, out, err = tf.fmt(diff=True)
assert ret == 0 assert ret == 0

View file

@ -0,0 +1,20 @@
variable "ami" {
default = "foo"
type = "string"
}
variable "list" {
default = []
type = "list"
}
variable "map" {
default = {}
type = "map"
}
resource "aws_instance" "bar" {
foo = "${var.ami}"
bar = "${join(",", var.list)}"
baz = "${join(",", keys(var.map))}"
}