commit
896cc822bc
9 changed files with 130 additions and 163 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
## 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`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
1
VERSION
1
VERSION
|
@ -1 +0,0 @@
|
||||||
0.8.1
|
|
|
@ -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
|
||||||
|
|
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
|
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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
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