Compare commits

..

No commits in common. "master" and "1.0.2.dev3" have entirely different histories.

11 changed files with 78 additions and 164 deletions

5
.gitignore vendored
View file

@ -12,9 +12,6 @@ dist/
build/
/pytestdebug.log
.pytestdebug.log
/pytest_cache
.lsp
# virtualenv
.virtualenv/
@ -23,7 +20,6 @@ venv/
# Intellij
.idea
.idea/
# VSCode
.vscode/
@ -36,4 +32,3 @@ tmp.txt
/.tox/
env/
Icon
.clj-kondo

View file

@ -4,7 +4,7 @@ before_script:
- python --version
- pip install setuptools wheel twine
- pip install .
- pip install -r requirements_dev.txt
- pip install -r dev_requirements.txt
stages:
- lint
@ -16,21 +16,21 @@ flake8:
stage: lint
allow_failure: true
script:
- flake8 --max-line-length=120 --count --select=E9,F63,F7,F82 --show-source --statistics dda_python_terraform/*.py
- flake8 --count --exit-zero --max-complexity=13 --max-line-length=127 --statistics --ignore F401 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=10 --max-line-length=127 --statistics python_terraform/*.py
mypy:
stage: lint
allow_failure: true
script:
- python -m mypy dda_python_terraform/terraform.py
- python -m mypy dda_python_terraform/tfstate.py
- python -m mypy python_terraform/terraform.py
- python -m mypy python_terraform/tfstate.py
pylint:
stage: lint
allow_failure: true
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:
@ -81,7 +81,7 @@ test-1.1.3:
build:
stage: build
rules:
- if: '$CI_COMMIT_TAG =~ /^release-.*$/'
- if: '$CI_COMMIT_TAG != null'
artifacts:
paths:
- dist/*
@ -91,7 +91,7 @@ build:
pypi:
stage: upload
rules:
- if: '$CI_COMMIT_TAG =~ /^release-.*$/'
- if: '$CI_COMMIT_TAG != null'
script:
- twine upload dist/*
@ -99,7 +99,7 @@ gitlab:
image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: upload
rules:
- if: '$CI_COMMIT_TAG =~ /^release-.*$/'
- if: '$CI_COMMIT_TAG != null'
artifacts:
paths:
- release/*

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
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/
[![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
pip install python-terraform
## Usage
#### For any terraform command
from dda_python_terraform import *
from python_terraform import *
t = Terraform()
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,
`import` here could be called by
from dda_python_terraform import *
from python_terraform import *
t = Terraform()
return_code, stdout, stderr = t.import_cmd(*arguments, **options)
or just call cmd method directly
from dda_python_terraform import *
from python_terraform import *
t = Terraform()
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`.
from dda_python_terraform import Terraform
from python_terraform import Terraform
t = Terraform()
return_code, stdout, stderr = t.<cmd_name>(capture_output=False)
@ -96,19 +96,19 @@ In shell:
In python-terraform:
from dda_python_terraform import *
from python_terraform import *
tf = Terraform(working_dir='/home/test')
tf.apply(no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'})
or
from dda_python_terraform import *
from python_terraform import *
tf = Terraform()
tf.apply('/home/test', no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'})
or
from dda_python_terraform import *
from python_terraform import *
tf = Terraform(working_dir='/home/test', variables={'a':'b', 'c':'d'})
tf.apply(no_color=IsFlagged, refresh=False)
@ -120,7 +120,7 @@ In shell:
In python-terraform:
from dda_python_terraform import *
from python_terraform import *
tf = terraform(working_dir='/home/test')
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.
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`
## 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,12 +0,0 @@
## Release
```
adjust version number in setup.py to release version number.
git commit -am "release"
git tag -am "release" release-[release version no]
git push --follow-tags
increase version no in setup.py
git commit -am "version bump"
git push
pip3 install --upgrade --user dda-python-terraform
```

View file

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

View file

@ -1,4 +1,3 @@
"""Module providing wrapper for terraform."""
import json
import logging
import os
@ -6,9 +5,8 @@ import subprocess
import sys
import tempfile
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
from packaging import version
from dda_python_terraform.tfstate import Tfstate
from python_terraform.tfstate import Tfstate
logger = logging.getLogger(__name__)
@ -31,9 +29,8 @@ CommandOutput = Tuple[Optional[int], Optional[str], Optional[str]]
class TerraformCommandError(subprocess.CalledProcessError):
"""Class representing a terraform error"""
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.err = err
logger.error("Error with command %s. Reason: %s", self.cmd, self.err)
@ -55,7 +52,7 @@ class Terraform:
var_file: Optional[str] = None,
terraform_bin_path: Optional[str] = None,
is_env_vars_included: bool = True,
terraform_semantic_version: Optional[str] = "0.13.0"
terraform_version: Optional[float] = 0.13
):
"""
:param working_dir: the folder of the working folder, if not given,
@ -72,18 +69,17 @@ class Terraform:
:param terraform_bin_path: binary path of terraform
:type is_env_vars_included: bool
:param is_env_vars_included: included env variables when calling terraform cmd
:param terrform_semantic_version encodes major.minor.patch version of terraform. Defaults to 0.13.0
"""
self.is_env_vars_included = is_env_vars_included
self.working_dir = working_dir
self.state = state
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.terraform_bin_path = (
terraform_bin_path if terraform_bin_path else "terraform"
)
self.terraform_semantic_version = terraform_semantic_version
self.terraform_version = terraform_version
self.var_file = var_file
self.temp_var_files = VariableFiles()
@ -160,7 +156,7 @@ class Terraform:
global_opts = self._generate_default_general_options(dir_or_plan)
default = kwargs.copy()
# force is no longer a flag in version >= 1.0
if version.parse(self.terraform_semantic_version) < version.parse("1.0.0"):
if self.terraform_version < 1.0:
default["force"] = force
default["auto-approve"] = True
options = self._generate_default_options(default)
@ -315,15 +311,15 @@ class Terraform:
if self.is_env_vars_included:
environ_vars = os.environ.copy()
proc = subprocess.Popen(
p = subprocess.Popen(
cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars
)
if not synchronous:
return None, None, None
out, err = proc.communicate()
ret_code = proc.returncode
out, err = p.communicate()
ret_code = p.returncode
logger.info("output: %s", out)
if ret_code == 0:
@ -441,43 +437,15 @@ class Terraform:
global_opts = self._generate_default_general_options(False)
return self.cmd(global_opts, "workspace", "show", **kwargs)
def list_workspace(self) -> List[str]:
"""List of workspaces
:return: workspaces
:example:
>>> tf = Terraform()
>>> tf.list_workspace()
['default', 'test']
"""
global_opts = self._generate_default_general_options(False)
return list(
filter(
lambda workspace: len(workspace) > 0,
map(
lambda workspace: workspace.strip('*').strip(),
(self.cmd(global_opts, "workspace", "list")[1] or '').split()
)
)
)
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 (self.terraform_version < 1.0 and 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:
return []
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 os.path.isdir(self.working_dir + '/' + dir_or_plan):
if (self.terraform_version >= 1.0 and 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:
return {}
@ -496,6 +464,8 @@ class Terraform:
}
def _generate_cmd_options(self, **kwargs) -> List[str]:
"""
"""
result = []
for option, value in kwargs.items():
@ -509,12 +479,13 @@ class Terraform:
if isinstance(value, dict):
if "backend-config" in option:
for backend_key, backend_value in value.items():
result += [f"-backend-config={backend_key}={backend_value}"]
for bk, bv in value.items():
result += [f"-backend-config={bk}={bv}"]
continue
# since map type sent in string won't work, create temp var file for
# 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.
# An empty var-file would result in an error: An argument or block definition is required here
if value:
@ -552,13 +523,13 @@ class Terraform:
return wrapper
class VariableFiles:
"""Class representing a terraform var files"""
def __init__(self):
self.files = []
def create(self, variables: Dict[str, str]) -> str:
"""create var file in temp"""
with tempfile.NamedTemporaryFile(
"w+t", suffix=".tfvars.json", delete=False
) as temp:
@ -571,8 +542,7 @@ class VariableFiles:
return file_name
def clean_up(self):
"""cleanup the var file"""
for fle in self.files:
os.unlink(fle.name)
for f in self.files:
os.unlink(f.name)
self.files = []

View file

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

View file

@ -1 +0,0 @@
packaging

View file

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

View file

@ -6,19 +6,19 @@ import shutil
from contextlib import contextmanager
from io import StringIO
from typing import Callable
from packaging import version
import pytest
from _pytest.logging import LogCaptureFixture, caplog
from dda_python_terraform import IsFlagged, IsNotFlagged, Terraform, TerraformCommandError
from python_terraform import IsFlagged, IsNotFlagged, Terraform, TerraformCommandError
logging.basicConfig(level=logging.DEBUG)
root_logger = logging.getLogger()
current_path = os.path.dirname(os.path.realpath(__file__))
semantic_version = os.environ.get("TFVER")
version = 1.0 if (os.environ.get("TFVER") and os.environ.get(
"TFVER").startswith("1")) else 0.13
FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS = "test 'test.out!"
STRING_CASES = [
@ -60,7 +60,7 @@ CMD_CASES_0_x = [
var={"test_var": "test"},
raise_on_error=False,
),
# Expected output varies by terraform semantic_version
# Expected output varies by terraform version
"Plan: 0 to add, 0 to change, 0 to destroy.",
0,
False,
@ -131,7 +131,7 @@ CMD_CASES_1_x = [
var={"test_var": "test"},
raise_on_error=False,
),
# Expected output varies by terraform semantic_version
# Expected output varies by terraform version
"Changes to Outputs:",
0,
False,
@ -236,7 +236,7 @@ def workspace_setup_teardown():
@contextmanager
def wrapper(workspace_name, create=True, delete=True, *args, **kwargs):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=current_path, terraform_version=version)
tf.init()
if create:
tf.create_workspace(workspace_name, *args, **kwargs)
@ -271,14 +271,14 @@ class TestTerraform:
@pytest.mark.parametrize(["method", "expected"], STRING_CASES)
def test_generate_cmd_string(self, method: Callable[..., str], expected: str):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=current_path, terraform_version=version)
result = method(tf)
strs = expected.split()
for s in strs:
assert s in result
@pytest.mark.parametrize(*(CMD_CASES_1_x if version.parse(semantic_version) >= version.parse("1.0.0") else CMD_CASES_0_x))
@pytest.mark.parametrize(*(CMD_CASES_1_x if version >= 1.0 else CMD_CASES_0_x))
def test_cmd(
self,
method: Callable[..., str],
@ -290,7 +290,7 @@ class TestTerraform:
folder: str,
):
with caplog.at_level(logging.INFO):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=current_path, terraform_version=version)
tf.init(folder)
try:
ret, out, _ = method(tf)
@ -305,10 +305,10 @@ class TestTerraform:
assert expected_logs in caplog.text
@pytest.mark.parametrize(*(APPLY_CASES_1_x if version.parse(semantic_version) >= version.parse("1.0.0") else APPLY_CASES_0_x))
@pytest.mark.parametrize(*(APPLY_CASES_1_x if version >= 1.0 else APPLY_CASES_0_x))
def test_apply(self, folder, variables, var_files, expected_output, options):
tf = Terraform(
working_dir=current_path, variables=variables, var_file=var_files, terraform_semantic_version=semantic_version
working_dir=current_path, variables=variables, var_file=var_files, terraform_version=version
)
tf.init(folder)
ret, out, err = tf.apply(folder, **options)
@ -316,29 +316,9 @@ class TestTerraform:
assert expected_output in out.replace("\n", "").replace(" ", "")
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):
with caplog.at_level(logging.INFO):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=current_path, terraform_version=version)
folder = "var_to_output"
tf.init(folder)
tf.apply(
@ -358,7 +338,7 @@ class TestTerraform:
],
)
def test_options(self, cmd, args, options, fmt_test_file):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=current_path, terraform_version=version)
ret, out, err = getattr(tf, cmd)(*args, **options)
assert ret == 0
assert out == ""
@ -366,26 +346,26 @@ class TestTerraform:
def test_state_data(self):
cwd = os.path.join(current_path, "test_tfstate_file")
tf = Terraform(working_dir=cwd, state="tfstate.test",
terraform_semantic_version=semantic_version)
terraform_version=version)
tf.read_state_file()
assert tf.tfstate.modules[0]["path"] == ["root"]
def test_state_default(self):
cwd = os.path.join(current_path, "test_tfstate_file2")
tf = Terraform(working_dir=cwd, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=cwd, terraform_version=version)
tf.read_state_file()
assert tf.tfstate.modules[0]["path"] == ["default"]
def test_state_default_backend(self):
cwd = os.path.join(current_path, "test_tfstate_file3")
tf = Terraform(working_dir=cwd, terraform_semantic_version=semantic_version)
tf = Terraform(working_dir=cwd, terraform_version=version)
tf.read_state_file()
assert tf.tfstate.modules[0]["path"] == ["default_backend"]
def test_pre_load_state_data(self):
cwd = os.path.join(current_path, "test_tfstate_file")
tf = Terraform(working_dir=cwd, state="tfstate.test",
terraform_semantic_version=semantic_version)
terraform_version=version)
assert tf.tfstate.modules[0]["path"] == ["root"]
@pytest.mark.parametrize(
@ -393,7 +373,7 @@ class TestTerraform:
)
def test_override_default(self, folder, variables):
tf = Terraform(working_dir=current_path,
variables=variables, terraform_semantic_version=semantic_version)
variables=variables, terraform_version=version)
tf.init(folder)
ret, out, err = tf.apply(
folder, var={"test_var": "test2"}, no_color=IsNotFlagged,
@ -409,7 +389,7 @@ class TestTerraform:
required_output = "test_output"
with caplog.at_level(logging.INFO):
tf = Terraform(
working_dir=current_path, variables={"test_var": expected_value}, terraform_semantic_version=semantic_version
working_dir=current_path, variables={"test_var": expected_value}, terraform_version=version
)
tf.init("var_to_output")
tf.apply("var_to_output")
@ -423,7 +403,7 @@ class TestTerraform:
def test_destroy(self):
tf = Terraform(working_dir=current_path, variables={
"test_var": "test"}, terraform_semantic_version=semantic_version)
"test_var": "test"}, terraform_version=version)
tf.init("var_to_output")
ret, out, err = tf.destroy("var_to_output")
assert ret == 0
@ -434,7 +414,7 @@ class TestTerraform:
)
def test_plan(self, plan, variables, expected_ret):
tf = Terraform(working_dir=current_path,
variables=variables, terraform_semantic_version=semantic_version)
variables=variables, terraform_version=version)
tf.init(plan)
with pytest.raises(TerraformCommandError) as e:
tf.plan(plan)
@ -444,7 +424,7 @@ class TestTerraform:
def test_fmt(self, fmt_test_file):
tf = Terraform(working_dir=current_path, variables={
"test_var": "test"}, terraform_semantic_version=semantic_version)
"test_var": "test"}, terraform_version=version)
ret, out, err = tf.fmt(diff=True)
assert ret == 0
@ -549,9 +529,3 @@ class TestTerraform:
in caplog.messages
)
"""
def test_list_workspace(self):
tf = Terraform(working_dir=current_path)
workspaces = tf.list_workspace()
assert len(workspaces) > 0
assert 'default' in workspaces