commit
896cc822bc
9 changed files with 130 additions and 163 deletions
|
@ -30,6 +30,12 @@ script:
|
|||
- "export PATH=$PATH:$PWD/tf_bin"
|
||||
- tox
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- release/**
|
||||
|
||||
deploy:
|
||||
# test pypi
|
||||
- provider: pypi
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
include VERSION
|
43
README.md
43
README.md
|
@ -9,15 +9,6 @@ python-terraform is a python module provide a wrapper of `terraform` command lin
|
|||
## Installation
|
||||
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
|
||||
####For any terraform command
|
||||
|
||||
|
@ -25,10 +16,13 @@ like `-no-color` and `True/False` value reserved for option like
|
|||
t = Terraform()
|
||||
return_code, stdout, stderr = t.<cmd_name>(*arguments, **options)
|
||||
|
||||
####For any parameter
|
||||
simply pass as argument in order of method, for example,
|
||||
####For any argument
|
||||
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
|
||||
|
||||
|
@ -86,6 +80,12 @@ or
|
|||
tf = Terraform()
|
||||
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
|
||||
In shell:
|
||||
|
||||
|
@ -98,6 +98,25 @@ In python-terraform:
|
|||
tf = terraform(working_dir='/home/test')
|
||||
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`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
1
VERSION
1
VERSION
|
@ -1 +0,0 @@
|
|||
0.8.1
|
|
@ -34,13 +34,17 @@ class Terraform(object):
|
|||
var_file=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 state: path of state file relative to working folder
|
||||
:param variables: variables for apply/destroy/plan command
|
||||
:param parallelism: parallelism 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
|
||||
as default value of apply/destroy/plan command
|
||||
:param state: path of state file relative to working folder,
|
||||
as a default value of apply/destroy/plan command
|
||||
:param variables: default variables for apply/destroy/plan command,
|
||||
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
|
||||
"""
|
||||
self.working_dir = working_dir
|
||||
|
@ -64,21 +68,28 @@ class Terraform(object):
|
|||
|
||||
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
|
||||
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 kwargs: same as kwags in method 'cmd'
|
||||
:returns return_code, stdout, stderr
|
||||
"""
|
||||
default = dict()
|
||||
args, option_dict = self._create_cmd_args(dir_or_plan, default, kwargs)
|
||||
|
||||
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)
|
||||
|
||||
def _create_cmd_args(self, dir_or_plan, default_dict, kwargs):
|
||||
option_dict = default_dict
|
||||
def _generate_default_args(self, dir_or_plan):
|
||||
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['target'] = self.targets
|
||||
option_dict['var'] = self.variables
|
||||
|
@ -86,19 +97,34 @@ class Terraform(object):
|
|||
option_dict['parallelism'] = self.parallelism
|
||||
option_dict['no_color'] = IsFlagged
|
||||
option_dict['input'] = False
|
||||
option_dict.update(kwargs)
|
||||
args = [dir_or_plan] if dir_or_plan else []
|
||||
return args, option_dict
|
||||
option_dict.update(input_options)
|
||||
return 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
|
||||
force/no-color option is flagged by default
|
||||
:return: ret_code, stdout, stderr
|
||||
"""
|
||||
default = {'force': IsFlagged}
|
||||
args, option_dict = self._create_cmd_args(dir_or_plan, default, kwargs)
|
||||
return self.cmd('destroy', *args, **option_dict)
|
||||
default = kwargs
|
||||
default['force'] = force
|
||||
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):
|
||||
"""
|
||||
|
@ -149,15 +175,12 @@ class Terraform(object):
|
|||
cmds += ['-{k}'.format(k=k)]
|
||||
continue
|
||||
|
||||
if v is IsNotFlagged:
|
||||
if v is None or v is IsNotFlagged:
|
||||
continue
|
||||
|
||||
if type(v) is bool:
|
||||
v = 'true' if v else 'false'
|
||||
|
||||
if not v:
|
||||
continue
|
||||
|
||||
cmds += ['-{k}={v}'.format(k=k, v=v)]
|
||||
|
||||
cmds += args
|
||||
|
|
104
release.py
104
release.py
|
@ -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()
|
13
setup.py
13
setup.py
|
@ -2,7 +2,6 @@
|
|||
This is a python module provide a wrapper of terraform command line tool
|
||||
"""
|
||||
from setuptools import setup
|
||||
import os
|
||||
|
||||
dependencies = []
|
||||
module_name = 'python-terraform'
|
||||
|
@ -16,19 +15,9 @@ except IOError:
|
|||
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(
|
||||
name=module_name,
|
||||
version=get_version(),
|
||||
version='0.8.1',
|
||||
url='https://github.com/beelit94/python-terraform',
|
||||
license='MIT',
|
||||
author='Freddy Tan',
|
||||
|
|
|
@ -32,7 +32,8 @@ CMD_CASES = [
|
|||
]
|
||||
]
|
||||
|
||||
@pytest.fixture()
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def fmt_test_file(request):
|
||||
target = os.path.join(current_path, 'bad_fmt', 'test.backup')
|
||||
orgin = os.path.join(current_path, 'bad_fmt', 'test.tf')
|
||||
|
@ -79,16 +80,18 @@ class TestTerraform(object):
|
|||
assert ret == 0
|
||||
|
||||
@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", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]"),
|
||||
("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",
|
||||
{'test_var': 'test'}, None, "test_output=test", {}),
|
||||
("var_to_output", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]", {}),
|
||||
("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)
|
||||
ret, out, err = tf.apply(folder)
|
||||
ret, out, err = tf.apply(folder, **options)
|
||||
assert ret == 0
|
||||
assert expected_output in out.replace('\n', '').replace(' ', '')
|
||||
assert err == ''
|
||||
|
@ -129,6 +132,8 @@ class TestTerraform(object):
|
|||
no_color=IsNotFlagged)
|
||||
out = out.replace('\n', '')
|
||||
assert '\x1b[0m\x1b[1m\x1b[32mApply' in out
|
||||
out = tf.output('test_output')
|
||||
assert 'test2' in out
|
||||
|
||||
def test_get_output(self):
|
||||
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
|
||||
|
@ -141,7 +146,18 @@ class TestTerraform(object):
|
|||
assert ret == 0
|
||||
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'})
|
||||
ret, out, err = tf.fmt(diff=True)
|
||||
assert ret == 0
|
||||
|
|
20
test/vars_require_input/main.tf
Normal file
20
test/vars_require_input/main.tf
Normal 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))}"
|
||||
}
|
Loading…
Reference in a new issue