Compare commits

..

No commits in common. "master" and "release-2.0.1" have entirely different histories.

9 changed files with 43 additions and 88 deletions

4
.gitignore vendored
View file

@ -12,9 +12,6 @@ dist/
build/ build/
/pytestdebug.log /pytestdebug.log
.pytestdebug.log .pytestdebug.log
/pytest_cache
.lsp
# virtualenv # virtualenv
.virtualenv/ .virtualenv/
@ -36,4 +33,3 @@ tmp.txt
/.tox/ /.tox/
env/ env/
Icon Icon
.clj-kondo

View file

@ -16,21 +16,21 @@ flake8:
stage: lint stage: lint
allow_failure: true allow_failure: true
script: script:
- flake8 --max-line-length=120 --count --select=E9,F63,F7,F82 --show-source --statistics dda_python_terraform/*.py - flake8 --max-line-length=120 --count --select=E9,F63,F7,F82 --show-source --statistics python_terraform/*.py
- flake8 --count --exit-zero --max-complexity=13 --max-line-length=127 --statistics --ignore F401 dda_python_terraform/*.py - flake8 --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics python_terraform/*.py
mypy: mypy:
stage: lint stage: lint
allow_failure: true allow_failure: true
script: script:
- python -m mypy dda_python_terraform/terraform.py - python -m mypy python_terraform/terraform.py
- python -m mypy dda_python_terraform/tfstate.py - python -m mypy python_terraform/tfstate.py
pylint: pylint:
stage: lint stage: lint
allow_failure: true allow_failure: true
script: script:
- pylint -d C0112,C0115,C0301,R0913,R0903,R0902,R0914,R1705,R1732,W0622 dda_python_terraform/*.py - pylint -d C0301 python_terraform/*.py
test-0.13.7: test-0.13.7:

View file

@ -1,21 +1,21 @@
# dda-python-terraform
[![pipeline status](https://gitlab.com/domaindrivenarchitecture/dda-python-terraform/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/dda-python-terraform/-/commits/main)
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa.de/images/parts/contact/mastodon36_hue9b2464f10b18e134322af482b9c915e_5501_filter_14705073121015236177.png" width=20 alt="M"> meissa@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@meissa) | [Blog](https://domaindrivenarchitecture.org) | [Website](https://meissa.de)
## Introduction ## Introduction
dda-python-terraform is a python module provide a wrapper of `terraform` command line tool. python-terraform is a python module provide a wrapper of `terraform` command line tool.
`terraform` is a tool made by Hashicorp, please refer to https://terraform.io/ `terraform` is a tool made by Hashicorp, please refer to https://terraform.io/
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
### Status
[![Build Status](https://travis-ci.org/aubustou/python-terraform.svg?branch=develop)](https://travis-ci.org/aubustou/python-terraform)
## Installation ## Installation
pip install python-terraform pip install python-terraform
## Usage ## Usage
#### For any terraform command #### For any terraform command
from dda_python_terraform import * from python_terraform import *
t = Terraform() t = Terraform()
return_code, stdout, stderr = t.<cmd_name>(*arguments, **options) return_code, stdout, stderr = t.<cmd_name>(*arguments, **options)
@ -23,13 +23,13 @@ dda-python-terraform is a python module provide a wrapper of `terraform` command
to be able to call the method, you could call cmd_name by adding `_cmd` after command name, for example, to be able to call the method, you could call cmd_name by adding `_cmd` after command name, for example,
`import` here could be called by `import` here could be called by
from dda_python_terraform import * from python_terraform import *
t = Terraform() t = Terraform()
return_code, stdout, stderr = t.import_cmd(*arguments, **options) return_code, stdout, stderr = t.import_cmd(*arguments, **options)
or just call cmd method directly or just call cmd method directly
from dda_python_terraform import * from python_terraform import *
t = Terraform() t = Terraform()
return_code, stdout, stderr = t.cmd(<cmd_name>, *arguments, **options) return_code, stdout, stderr = t.cmd(<cmd_name>, *arguments, **options)
@ -82,7 +82,7 @@ simply pass the string to arguments of the method, for example,
By default, stdout and stderr are captured and returned. This causes the application to appear to hang. To print terraform output in real time, provide the `capture_output` option with any value other than `None`. This will cause the output of terraform to be printed to the terminal in real time. The value of `stdout` and `stderr` below will be `None`. By default, stdout and stderr are captured and returned. This causes the application to appear to hang. To print terraform output in real time, provide the `capture_output` option with any value other than `None`. This will cause the output of terraform to be printed to the terminal in real time. The value of `stdout` and `stderr` below will be `None`.
from dda_python_terraform import Terraform from python_terraform import Terraform
t = Terraform() t = Terraform()
return_code, stdout, stderr = t.<cmd_name>(capture_output=False) return_code, stdout, stderr = t.<cmd_name>(capture_output=False)
@ -96,19 +96,19 @@ In shell:
In python-terraform: In python-terraform:
from dda_python_terraform import * from python_terraform import *
tf = Terraform(working_dir='/home/test') tf = Terraform(working_dir='/home/test')
tf.apply(no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'}) tf.apply(no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'})
or or
from dda_python_terraform import * from python_terraform import *
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 or
from dda_python_terraform import * from python_terraform import *
tf = Terraform(working_dir='/home/test', variables={'a':'b', 'c':'d'}) tf = Terraform(working_dir='/home/test', variables={'a':'b', 'c':'d'})
tf.apply(no_color=IsFlagged, refresh=False) tf.apply(no_color=IsFlagged, refresh=False)
@ -120,7 +120,7 @@ In shell:
In python-terraform: In python-terraform:
from dda_python_terraform import * from python_terraform import *
tf = terraform(working_dir='/home/test') tf = terraform(working_dir='/home/test')
tf.fmt(diff=True) tf.fmt(diff=True)
@ -140,12 +140,3 @@ 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. a exhaustive method implementation which I don't prefer to.
Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option 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` like `-no-color` and `True/False` value reserved for option like `refresh=true`
## Development & mirrors
Development happens at: https://repo.prod.meissa.de/meissa/dda-python-terraform
Mirrors are:
* https://gitlab.com/domaindrivenarchitecture/dda-python-terraform (CI issues and PR)
* https://github.com/DomainDrivenArchitecture/dda-python-terraform
For more details about our repository model see: https://repo.prod.meissa.de/meissa/federate-your-repos

View file

@ -1,4 +1,3 @@
"""Module providing wrapper for terraform."""
from .terraform import ( from .terraform import (
IsFlagged, IsFlagged,
IsNotFlagged, IsNotFlagged,

View file

@ -1,4 +1,3 @@
"""Module providing wrapper for terraform."""
import json import json
import logging import logging
import os import os
@ -31,9 +30,8 @@ CommandOutput = Tuple[Optional[int], Optional[str], Optional[str]]
class TerraformCommandError(subprocess.CalledProcessError): class TerraformCommandError(subprocess.CalledProcessError):
"""Class representing a terraform error"""
def __init__(self, ret_code: int, cmd: str, out: Optional[str], err: Optional[str]): def __init__(self, ret_code: int, cmd: str, out: Optional[str], err: Optional[str]):
super().__init__(ret_code, cmd) super(TerraformCommandError, self).__init__(ret_code, cmd)
self.out = out self.out = out
self.err = err self.err = err
logger.error("Error with command %s. Reason: %s", self.cmd, self.err) logger.error("Error with command %s. Reason: %s", self.cmd, self.err)
@ -78,7 +76,7 @@ class Terraform:
self.working_dir = working_dir self.working_dir = working_dir
self.state = state self.state = state
self.targets = [] if targets is None else targets self.targets = [] if targets is None else targets
self.variables = {} if variables is None else variables self.variables = dict() if variables is None else variables
self.parallelism = parallelism self.parallelism = parallelism
self.terraform_bin_path = ( self.terraform_bin_path = (
terraform_bin_path if terraform_bin_path else "terraform" terraform_bin_path if terraform_bin_path else "terraform"
@ -315,15 +313,15 @@ class Terraform:
if self.is_env_vars_included: if self.is_env_vars_included:
environ_vars = os.environ.copy() environ_vars = os.environ.copy()
proc = subprocess.Popen( p = subprocess.Popen(
cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars
) )
if not synchronous: if not synchronous:
return None, None, None return None, None, None
out, err = proc.communicate() out, err = p.communicate()
ret_code = proc.returncode ret_code = p.returncode
logger.info("output: %s", out) logger.info("output: %s", out)
if ret_code == 0: if ret_code == 0:
@ -464,20 +462,12 @@ class Terraform:
def _generate_default_args(self, dir_or_plan: Optional[str]) -> Sequence[str]: def _generate_default_args(self, dir_or_plan: Optional[str]) -> Sequence[str]:
if (version.parse(self.terraform_semantic_version) < version.parse("1.0.0") and dir_or_plan): if (version.parse(self.terraform_semantic_version) < version.parse("1.0.0") and dir_or_plan):
return [dir_or_plan] return [dir_or_plan]
elif (version.parse(self.terraform_semantic_version) >= version.parse("1.0.0") and dir_or_plan and os.path.isfile(f'{self.working_dir}/{dir_or_plan}')):
plan = dir_or_plan.split('/')[-1]
return [plan]
else: else:
return [] return []
def _generate_default_general_options(self, dir_or_plan: Optional[str]) -> Dict[str, Any]: def _generate_default_general_options(self, dir_or_plan: Optional[str]) -> Dict[str, Any]:
if (version.parse(self.terraform_semantic_version) >= version.parse("1.0.0") and dir_or_plan): if (version.parse(self.terraform_semantic_version) >= version.parse("1.0.0") and dir_or_plan):
if os.path.isdir(self.working_dir + '/' + dir_or_plan): return {"chdir": dir_or_plan}
return {"chdir": dir_or_plan}
else:
plan_path = dir_or_plan.split('/')
dir_to_plan_path = "/".join(plan_path[:-1])
return {"chdir": dir_to_plan_path}
else: else:
return {} return {}
@ -496,6 +486,8 @@ class Terraform:
} }
def _generate_cmd_options(self, **kwargs) -> List[str]: def _generate_cmd_options(self, **kwargs) -> List[str]:
"""
"""
result = [] result = []
for option, value in kwargs.items(): for option, value in kwargs.items():
@ -509,12 +501,13 @@ class Terraform:
if isinstance(value, dict): if isinstance(value, dict):
if "backend-config" in option: if "backend-config" in option:
for backend_key, backend_value in value.items(): for bk, bv in value.items():
result += [f"-backend-config={backend_key}={backend_value}"] result += [f"-backend-config={bk}={bv}"]
continue continue
# since map type sent in string won't work, create temp var file for # since map type sent in string won't work, create temp var file for
# variables, and clean it up later # variables, and clean it up later
if option == "var": elif option == "var":
# We do not create empty var-files if there is no var passed. # We do not create empty var-files if there is no var passed.
# An empty var-file would result in an error: An argument or block definition is required here # An empty var-file would result in an error: An argument or block definition is required here
if value: if value:
@ -552,13 +545,12 @@ class Terraform:
return wrapper return wrapper
class VariableFiles: class VariableFiles:
"""Class representing a terraform var files"""
def __init__(self): def __init__(self):
self.files = [] self.files = []
def create(self, variables: Dict[str, str]) -> str: def create(self, variables: Dict[str, str]) -> str:
"""create var file in temp"""
with tempfile.NamedTemporaryFile( with tempfile.NamedTemporaryFile(
"w+t", suffix=".tfvars.json", delete=False "w+t", suffix=".tfvars.json", delete=False
) as temp: ) as temp:
@ -571,8 +563,7 @@ class VariableFiles:
return file_name return file_name
def clean_up(self): def clean_up(self):
"""cleanup the var file""" for f in self.files:
for fle in self.files: os.unlink(f.name)
os.unlink(fle.name)
self.files = [] self.files = []

View file

@ -1,4 +1,3 @@
"""Helper Module providing wrapper for terraform state."""
import json import json
import logging import logging
import os import os
@ -8,7 +7,6 @@ logger = logging.getLogger(__name__)
class Tfstate: class Tfstate:
"""Class representing a terraform state"""
def __init__(self, data: Optional[Dict[str, str]] = None): def __init__(self, data: Optional[Dict[str, str]] = None):
self.tfstate_file: Optional[str] = None self.tfstate_file: Optional[str] = None
self.native_data = data self.native_data = data
@ -23,8 +21,8 @@ class Tfstate:
""" """
logger.debug("read data from %s", file_path) logger.debug("read data from %s", file_path)
if os.path.exists(file_path): if os.path.exists(file_path):
with open(file_path, encoding="utf-8") as fle: with open(file_path) as f:
json_data = json.load(fle) json_data = json.load(f)
tf_state = Tfstate(json_data) tf_state = Tfstate(json_data)
tf_state.tfstate_file = file_path tf_state.tfstate_file = file_path

View file

@ -1,7 +1,7 @@
## Release ## Release
``` ```
adjust version number in setup.py to release version number. adjust version no in setup.py to release version no.
git commit -am "release" git commit -am "release"
git tag -am "release" release-[release version no] git tag -am "release" release-[release version no]
git push --follow-tags git push --follow-tags

View file

@ -21,11 +21,11 @@ except IOError:
setup( setup(
name=module_name, name=module_name,
version="2.1.2-dev", version="2.0.1",
url="https://repo.prod.meissa.de/meissa/dda-python-terraform", url="https://github.com/DomainDrivenArchitecture/python-terraform",
license="MIT", license="MIT",
author="Freddy Tan, meissa team", author="Freddy Tan",
author_email="buero@meissa.de", author_email="beelit94@gmail.com",
description=short_description, description=short_description,
long_description=long_description, long_description=long_description,
packages=["dda_python_terraform"], packages=["dda_python_terraform"],

View file

@ -316,26 +316,6 @@ class TestTerraform:
assert expected_output in out.replace("\n", "").replace(" ", "") assert expected_output in out.replace("\n", "").replace(" ", "")
assert err == "" assert err == ""
def test_apply_plan(self):
# test is only applicable to version > 1.0.0
if version.parse(semantic_version) < version.parse("1.0.0"):
return
tf = Terraform(
working_dir=current_path, terraform_semantic_version=semantic_version
)
out_folder = 'var_to_output'
out_file_name = 'test.out'
out_file_path = f'{out_folder}/{out_file_name}'
tf.init(out_folder)
ret, _, err = tf.plan(out_folder, detailed_exitcode=IsNotFlagged, out=out_file_name)
assert ret == 0
assert err == ""
ret, _, err = tf.apply(out_file_path, skip_plan=True)
assert ret == 0
assert err == ""
def test_apply_with_var_file(self, caplog: LogCaptureFixture): def test_apply_with_var_file(self, caplog: LogCaptureFixture):
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version) tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)