Compare commits

..

163 commits

Author SHA1 Message Date
7c4cd23e56 [Skip-CI] Fix mastodon add website link 2024-08-06 15:07:51 +02:00
bom
3fd4a72d3d Adjust logic for plan files (version > 1.0.0)
We will no longer try to chdir into files and instead
add them as subcommands.
Includes regression test

Resolves #3 (https://gitlab.com/domaindrivenarchitecture/dda-python-terraform/-/issues/3)
2023-11-24 22:08:38 +01:00
0fb8218dbf [Skip-CI] Add Development and mirrors section 2023-07-28 14:30:52 +02:00
3c92a8b2e6 Change wording 2023-02-15 14:38:56 +01:00
be63bfde3e update doku 2023-01-27 13:20:33 +01:00
a3d97a0d04 fix most pylints 2023-01-27 11:19:03 +01:00
c1c25a016f fix flake8 2023-01-27 10:47:01 +01:00
0abb2370e4 version bump 2023-01-27 10:46:53 +01:00
5af484bcce release 2023-01-27 10:35:48 +01:00
722ba5f716 update the setup.py url to point to the new repository
See merge request domaindrivenarchitecture/dda-python-terraform!3
2023-01-23 16:57:54 +00:00
Marc Seiler
d88670503b update the setup.py url to point to the new repository 2023-01-23 16:57:54 +00:00
bc27522fb5 adjust readme 2022-08-19 15:50:09 +02:00
441bc497d1 version bump 2022-08-19 14:12:20 +02:00
ddb8e4160d release 2022-08-19 14:11:41 +02:00
71c544bad8 Merge branch 'use-semantic-version--string-instead-of-float' into 'master'
[breaking change] use semantic version string

See merge request domaindrivenarchitecture/python-terraform!2
2022-08-19 10:16:40 +00:00
1a7955b738 [breaking change] use semantic version string 2022-08-19 10:16:40 +00:00
9c9d7dc14b add list workspace 2022-08-05 18:09:17 +02:00
f94f3a7c43 adjust gitignore 2022-08-05 18:08:56 +02:00
026e5f260a adjust dev_requirements name 2022-08-05 17:48:19 +02:00
bom
35b52fdbe8 version bump 2022-01-28 09:26:05 +01:00
bom
d44570e9bb release 2022-01-28 09:24:38 +01:00
bom
5e204617d0 fixed releasing doc 2022-01-28 09:23:58 +01:00
bom
7d50eec689 use dda_python_terraform module 2022-01-28 09:17:28 +01:00
bom
b509685c62 rename package 2022-01-27 17:12:33 +01:00
bom
30daf13244 renamed python_terraform to dda_python_terraform 2022-01-27 17:03:30 +01:00
jem
fd431b229d version bump 2022-01-21 13:39:49 +01:00
jem
58e4e9f065 release 2022-01-21 13:37:07 +01:00
jem
124300c271 fix some pylints 2022-01-21 13:36:42 +01:00
jem
382c571d42 release 2022-01-21 13:31:00 +01:00
jem
cb30395d4d release 2022-01-21 13:26:46 +01:00
jem
dcbeb0e934 use protected tags for releasing 2022-01-21 13:23:50 +01:00
jem
9dd3664d8c release 2022-01-21 09:31:40 +01:00
jem
ae6bd30923 release 2022-01-21 09:24:22 +01:00
jem
ffbd97fb8f prepare release 2022-01-21 09:16:27 +01:00
jem
d5ad3c7628 release & upload only on tags 2022-01-20 19:06:33 +01:00
jem
4ba2857db7 build artifacts 2022-01-20 19:00:04 +01:00
jem
262685efda try to upload a release 2022-01-20 18:53:18 +01:00
jem
9863c3c115 test the build 2022-01-20 09:30:51 +01:00
jem
5bdfb662bc fix flake & add upload stage 2022-01-19 18:08:22 +01:00
bom
73b8fb0707 changed tests from 0.13.4 to 1.1.3 2022-01-19 17:23:10 +01:00
bom
09139ba1b1 fixed last test 2022-01-19 17:19:05 +01:00
bom
e92972db08 hopefully fixed last test 2022-01-19 17:15:59 +01:00
bom
7821fb431a fixed one apply test for 0.x 2022-01-19 17:06:14 +01:00
bom
2d57a67c63 fixed apply tests for 0.x 2022-01-19 16:51:07 +01:00
bom
816b76070e fixed last test 2022-01-19 16:33:06 +01:00
bom
4483f54987 fixed a plan test 2022-01-19 16:26:50 +01:00
bom
42b4f18b3f fixed destroy test 2022-01-12 15:00:30 +01:00
bom
fd959877a7 fixed last apply test 2022-01-12 14:32:47 +01:00
bom
f8f3e41529 fixed apply tests 2022-01-12 14:28:54 +01:00
bom
e3e5ad775f fixed another test_cmd case 2022-01-12 13:58:52 +01:00
bom
021db11acb fixed plan cmd test 2022-01-12 13:55:11 +01:00
bom
46b20fe086 removed obsolete tests 2022-01-12 13:28:04 +01:00
bom
3d2c7ae396 fixed output tests 2022-01-12 13:07:46 +01:00
bom
412ea860f8 updated tests with global_opts 2022-01-12 10:34:35 +01:00
jem
085273a726 one more test green 2021-11-09 09:18:49 +01:00
jem
f913b1d6b7 default for compatibility 2021-11-05 13:27:25 +01:00
jem
ee097cb230 cmd now works for 0.13 again 2021-10-23 16:22:59 +02:00
jem
5d1ee33790 wip 2021-10-23 15:42:43 +02:00
jem
607cdaf408 adjust cmd to new 1.0 release 2021-10-23 15:37:29 +02:00
jem
9c73aaec5e may not be used in vc 2021-10-15 17:51:15 +02:00
jem
fbc603d1f9 test more tf versions 2021-10-15 17:09:10 +02:00
jem
de7feeee19 add some more linting 2021-10-15 17:07:06 +02:00
jem
6048bf41d5 fix ci 2021-10-15 16:59:52 +02:00
jem
e84ebc2bfb add needed dev requirements 2021-10-15 16:48:16 +02:00
jem
af20c6da16 add pytest 2021-10-15 16:37:59 +02:00
jem
f2bb91560a starting with gitlab-ci 2021-10-15 16:33:09 +02:00
jem
9389fd5a24 ignore 2021-07-02 13:54:10 +02:00
jem
437f372fa3 version bump 2021-06-25 17:06:41 +02:00
jem
d8351ba873 ignore dist 2021-06-25 17:06:20 +02:00
jem
b3dfcb70e0 release 2021-06-25 17:04:24 +02:00
jem
f46b890778 ignore dist 2021-06-25 17:03:46 +02:00
jem
4ab98db4aa update changelog 2021-06-25 16:54:13 +02:00
jem
d6a3e3e722 version bump 2021-06-25 16:52:45 +02:00
jem
ed882822e7 release 2021-06-25 16:51:38 +02:00
jem
4c17cb661b release 2021-06-25 16:50:39 +02:00
jem
adb1fa06a4 add refresh & option to output latest cmd 2021-06-25 16:50:06 +02:00
jem
06663390c2 fix tests#1 2021-06-25 15:21:00 +02:00
jem
40a7cba90d post-fork-work 2021-06-25 13:36:20 +02:00
beelit94
b80c977c79
Merge pull request #106 from kirankotari/develop
Python packaging action to PyPI
2021-06-04 12:36:48 +08:00
Kiran Kumar Kotari
80ca26b26a
Create pytest.yml 2021-06-01 07:16:52 +02:00
Kiran Kumar Kotari
f6003d8e2f
Create python-publish.yml
Auto publish python package to pypi
2021-05-27 11:39:22 +02:00
Spikeophant
e1ae747ebf
Merge pull request #97 from aubustou/develop
Support for Terraform 0.14 and Python 3.6+
2021-03-01 07:14:42 -08:00
Francois Lebreau
edfc33b0bd Release 0.14.0 2021-02-28 14:02:43 +00:00
Francois Lebreau
7080f38a93 Support Terraform 0.14 2021-02-28 13:44:58 +00:00
Francois Lebreau
785e9a3ce7 Fix tests 2021-01-21 10:07:00 +00:00
Francois Lebreau
44675d38f5 Release 0.12.0 2021-01-21 01:21:48 +00:00
Francois Lebreau
bcd523ff67 Add typing 2021-01-21 01:01:34 +00:00
aubustou
b373f85311 Remove Python 3.9 as it is not yet available on Travis 2020-10-21 00:48:31 +02:00
aubustou
053f956774 Remove tox 2020-10-21 00:39:43 +02:00
aubustou
d83132b61a Bump version: 0.10.2 → 0.11.0 2020-10-21 00:28:55 +02:00
aubustou
09120a943b Fix bumpversion 2020-10-21 00:28:52 +02:00
aubustou
ba72a09409 Update README 2020-10-21 00:15:14 +02:00
aubustou
134786ebf0 Update isort 2020-10-21 00:10:17 +02:00
aubustou
6ea1320bef Fix unit tests for Terraform 0.13 2020-10-21 00:08:58 +02:00
aubustou
151c5dc92a Add pre-commit and apply Black format 2020-10-20 00:30:02 +02:00
aubustou
61a76803d4 Bump python version in travis 2020-10-20 00:24:47 +02:00
aubustou
ffd9e1de2a Fix tests 2020-10-20 00:24:33 +02:00
aubustou
47cec4d211 Bump Terraform version in travis 2020-10-20 00:24:16 +02:00
aubustou
fc584c2a48 Migrate to Python 3 only 2020-10-19 23:57:22 +02:00
Spikeophant
a33aebce1c
Merge pull request #74 from mwidera/fix-rf-stdout-stderr
Fix for subprocess call for Robot Framework execution
2019-11-07 06:54:15 -08:00
Michal Widera
632b653b2b Fix for subprocess call for Robot Framework execution
This change modifies behavior of capture_output flag to work with
robot framework. When set to given parameter it'll set stderr and stdout
to None for subprocess command call.
This fixes problem with error: "UnsupportedOperation: fileno"

Signed-off-by: Michal Widera <michal.widera@idemia.com>
2019-11-07 11:00:59 +01:00
beelit94
bda8b97e59
Merge pull request #69 from xishengcai/patch-1
fixing miss use of calling log object
2019-10-26 10:58:33 -07:00
caixisheng
6da2d853fc
Update __init__.py 2019-10-26 15:00:53 +08:00
Dustin Davisson
3efa8b2f65 updating gitignore for env dir from venv. 2019-10-16 13:25:54 -07:00
beelit94
e915a81f7c
Merge pull request #65 from 0x29a/develop
Replace deprecated warn with warning
2019-10-15 14:32:53 -07:00
noname
7c4ac21fb3 Replace deprecated warn with warning 2019-10-10 14:00:18 +03:00
Spike
89914bbbd8 Bumping version, adding vscode to .gitignore. Prepping for release of 0.10.2 2019-08-16 17:52:07 -07:00
Spikeophant
bcc1321563
Merge pull request #60 from szarya/hotfix/fix_synchronous_flag
Move synchrounous pop uphill, so it doesn't end up as a flag in the a…
2019-08-06 16:17:21 -07:00
Spikeophant
3eaaec37d0
Merge pull request #57 from BNMetrics/develop
fixed a bug with var-file argument, allow workspace commands to pass flags and options
2019-07-08 10:05:08 -07:00
BNMetrics
09fdcf2f11 fixed flaky tests 2019-06-25 22:12:41 +01:00
BNMetrics
c0ff5bf65a fixed terraform version and python version compat issue in test 2019-06-25 21:33:38 +01:00
BNMetrics
4a76a44c91 formatting on .gitignore 2019-06-25 19:57:03 +01:00
BNMetrics
f39b7e237e fixed a bug with var-file argument, allow workspace commands to pass flags and options 2019-06-25 19:53:39 +01:00
beelit94
a63e9dad24
Merge pull request #56 from beelit94/master
Master to develop
2019-06-21 00:07:02 -07:00
beelit94
76351ba371
Merge pull request #52 from beelit94/release/0.10.1
release/0.10.1
2019-06-20 23:43:10 -07:00
beelit94
ce788acc3f Bump version: 0.10.0 → 0.10.1 2019-06-20 23:42:16 -07:00
beelit94
6d8dde7991 1. Updating test env and Contribution doc
2. adding change log for 0.10.1
2019-06-20 23:28:02 -07:00
beelit94
0a7162a5ce roll back terraform version 2019-06-20 23:22:28 -07:00
beelit94
172ab6b509 roll back python version 2019-06-20 23:04:30 -07:00
Stephan Zaria
7056a853da Move synchrounous pop uphill, so it doesn't end up as a flag in the actual terraform command 2019-06-10 09:59:40 -07:00
beelit94
c79e807028 adding change log for 0.10.1 2019-06-05 00:14:23 -07:00
beelit94
3a32241cf1 Updating test env and Contribution doc 2019-06-05 00:07:24 -07:00
beelit94
a05fc98e8d
Merge pull request #49 from Spikeophant/develop
Adding workspace support
2019-06-04 23:17:35 -07:00
DJDavisson
c6d37cbaeb Removing uncessary comment in __init__. 2019-05-31 11:49:32 -07:00
DJDavisson
35db7917b0 Fixing missing , 2019-05-31 11:36:53 -07:00
DJDavisson
29daa9c40a Modifying returns to use proper format. 2019-05-31 11:31:30 -07:00
beelit94
c46560eeed
Merge pull request #48 from jschoewe/develop
bugfix/terraform 12 var-file suffix
2019-05-30 23:15:27 -07:00
DJDavisson
c000d00504 Fixing tests. 2019-05-20 11:34:42 -07:00
DJDavisson
70920d828c Adding workspace deletion. 2019-05-08 10:55:53 -07:00
DJDavisson
01e3dbb05a Removing optiosn and args pass from state stuff, state stuff doesn't take options or args. 2019-05-07 12:11:28 -07:00
DJDavisson
3ddab331f0 Adding tests, 2 are failing, looking into how to handle. 2019-05-07 11:39:07 -07:00
DJDavisson
64e804e7a0 Removing uncessary workspace from __init__ Will add back with some checking in the future. 2019-05-07 09:55:13 -07:00
DJDavisson
0228de1a60 Cleanup. 2019-05-03 14:17:46 -07:00
DJDavisson
749bec79e4 Adding additional workspace items. 2019-05-03 13:59:48 -07:00
DJDavisson
57a80d7e71 Removing working dir. 2019-05-03 13:55:38 -07:00
DJDavisson
05c44c2cdc Adding workspace support. 2019-05-03 13:54:28 -07:00
John Schoewe
832fc9eb7d
Merge pull request #1 from jschoewe/bugfix/terraform-12-var-file-name-suffix
Added suffix variable
2019-04-09 22:57:46 -04:00
John Schoewe
576197d768 Added suffix variable 2019-04-09 22:54:15 -04:00
beelit94
813d23d759
Merge pull request #37 from beelit94/develop
release 0.10.0
2018-04-13 14:27:11 -07:00
beelit94
99950cb03c add release doc 2018-03-19 18:34:08 -07:00
beelit94
a27362a145 Bump version: 0.9.1 → 0.10.0 2018-03-19 18:30:49 -07:00
beelit94
e2b7bd4686
Merge pull request #18 from raquel-ucl/asynch
Return access to the subprocess so output can be handled as desired
2018-03-19 18:28:40 -07:00
beelit94
fe0e651607
Merge pull request #27 from waghanza/no_interaction
No interaction
2017-11-24 14:36:11 -08:00
Marwan Rabbâa
e9255b118c add force param to terraform apply 2017-11-24 22:36:44 +01:00
beelit94
bd6528e68e Merge pull request #24 from surround-io/develop
Full support for output(); support for raise_on_error
2017-10-16 10:22:06 -07:00
Sam McKelvie
98d221c779 address pull request feedback 2017-10-16 09:12:34 -07:00
Sam McKelvie
99c67e5fe5 Address pull request feedback 2017-10-16 09:03:45 -07:00
Sam McKelvie
4945f4591d Make tests pass with old and new terraform 2017-10-13 14:20:36 -07:00
Sam McKelvie
723ab0b79e Merge pull request #1 from surround-io/sammck-develop
Add full support for 'output' command, and enable raise_on_error option
2017-10-13 13:52:38 -07:00
Sam McKelvie
ec826887f5 Add full support for 'output' command, and enable raise_on_error option
Add a general "raise_on_error" option to all terraform commands. If provided and
set to anything that evaluates to True, then TerraformCommandError (a subclass of
subprocess.CalledProcessError) will be raised if the returncode is not 0. The exception
object will have the following special proerties:
    returncode: The returncode from the command, as in subprocess.CalledProcessError.
    out:        The contents of stdout if available, otherwise None
    err:        The contents of stderr if available, otherwise None

Terraform.output() no longer requires an argument for the output name; if omitted, it
returns a dict of all outputs, exactly as expected from 'terraform output -json'.

Terraform.output() now accepts an optional "full_value" option. If provided and True, and
an output name was provided, then the return value will be a dict with "value", "type",
and "sensitive" fields, exactly as expected from 'terraform output -json <output-name>'

Added tests for all of this new functionality...
2017-10-13 13:36:25 -07:00
beelit94
9afa4e1d4e Merge branch 'release/0.9.1' 2017-09-01 09:52:44 -07:00
beelit94
a4b36e1418 Merge branch 'release/0.9.1' into develop 2017-09-01 09:52:44 -07:00
beelit94
3c9ea8b526 Bump version: 0.9.0 → 0.9.1 2017-09-01 09:51:54 -07:00
beelit94
45860aa39c add release note 2017-09-01 09:51:37 -07:00
beelit94
042868bada Merge pull request #21 from beelit94/feature/minior_refactor
minor refactor
2017-08-30 11:06:37 -07:00
beelit94
68954d1447 Merge branch 'develop' into asynch 2017-08-30 11:04:59 -07:00
beelit94
b098b5a1d8 minor refactor 2017-08-30 11:00:15 -07:00
beelit94
4191b40cdc Merge pull request #20 from beelit94/python-terraform-10
add changelog handler
2017-08-28 11:46:39 -07:00
beelit94
86182a604f add changelog 2017-08-28 11:17:33 -07:00
beelit94
3b181bc403 Merge pull request #19 from jaustinpage/develop
Support for Terraform init, and using backend configs.
2017-08-28 11:07:17 -07:00
Austin Page
825fa0e54f
Adding init command and support for backend terraform state files 2017-08-23 11:35:33 -05:00
Raquel Alegre
da5e648e3f Move version up. 2017-08-11 19:00:22 +01:00
Raquel Alegre
0b2eb3b1be Return a reference to the subprocess so output can be handled from elsewhere. 2017-08-11 19:00:06 +01:00
29 changed files with 1528 additions and 648 deletions

View file

@ -1,7 +0,0 @@
[bumpversion]
current_version = 0.9.0
commit = True
tag = False
[bumpversion:file:setup.py]

33
.gitignore vendored
View file

@ -1,10 +1,39 @@
# terraform
*.tfstate *.tfstate
*.tfstate.backup *.tfstate.backup
# python
*.pyc *.pyc
*.egg-info *.egg-info
.idea
.cache .cache
__pycache__
/.pypirc /.pypirc
/.tox/ dist/
build/
/pytestdebug.log
.pytestdebug.log
/pytest_cache
.lsp
# virtualenv
.virtualenv/
venv/
# Intellij
.idea
.idea/
# VSCode
.vscode/
pyrightconfig.json
tmp.txt
# other
.dropbox .dropbox
.DS_Store
/.tox/
env/
Icon Icon
.clj-kondo

117
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,117 @@
image: "python:3.8"
before_script:
- python --version
- pip install setuptools wheel twine
- pip install .
- pip install -r requirements_dev.txt
stages:
- lint
- test
- build
- upload
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
mypy:
stage: lint
allow_failure: true
script:
- python -m mypy dda_python_terraform/terraform.py
- python -m mypy dda_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
test-0.13.7:
stage: test
script:
- export TFVER=0.13.7
- export TFURL=https://releases.hashicorp.com/terraform/
- TFURL+=$TFVER
- TFURL+="/terraform_"
- TFURL+=$TFVER
- TFURL+="_linux_amd64.zip"
- wget $TFURL -O terraform_bin.zip
- mkdir tf_bin
- unzip terraform_bin.zip -d tf_bin
- PATH=$PATH:$PWD/tf_bin
- pytest -v
test-1.0.8:
stage: test
script:
- export TFVER=1.0.8
- export TFURL=https://releases.hashicorp.com/terraform/
- TFURL+=$TFVER
- TFURL+="/terraform_"
- TFURL+=$TFVER
- TFURL+="_linux_amd64.zip"
- wget $TFURL -O terraform_bin.zip
- mkdir tf_bin
- unzip terraform_bin.zip -d tf_bin
- PATH=$PATH:$PWD/tf_bin
- pytest -v
test-1.1.3:
stage: test
script:
- export TFVER=1.1.3
- export TFURL=https://releases.hashicorp.com/terraform/
- TFURL+=$TFVER
- TFURL+="/terraform_"
- TFURL+=$TFVER
- TFURL+="_linux_amd64.zip"
- wget $TFURL -O terraform_bin.zip
- mkdir tf_bin
- unzip terraform_bin.zip -d tf_bin
- PATH=$PATH:$PWD/tf_bin
- pytest -v
build:
stage: build
rules:
- if: '$CI_COMMIT_TAG =~ /^release-.*$/'
artifacts:
paths:
- dist/*
script:
- python setup.py sdist bdist_wheel
pypi:
stage: upload
rules:
- if: '$CI_COMMIT_TAG =~ /^release-.*$/'
script:
- twine upload dist/*
gitlab:
image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: upload
rules:
- if: '$CI_COMMIT_TAG =~ /^release-.*$/'
artifacts:
paths:
- release/*
before_script:
- echo "upload to gitlab"
script:
- apk --no-cache add curl
- cp -r dist release
- cd release
- rm *.whl
- find . -type f -exec sha256sum {} \; | sort > sha256sum.lst
- find . -type f -exec sha512sum {} \; | sort > sha512sum.lst
- |
release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"release\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/python-terraform/-/jobs/${CI_JOB_ID}/artifacts/file/release\"}" \

27
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,27 @@
default_language_version:
python: python3.6
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0 # v2.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-docstring-first
- id: check-json
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: requirements-txt-fixer
- repo: https://github.com/pycqa/isort
rev: 5.5.2
hooks:
- id: isort
- repo: https://github.com/lovesegfault/beautysh
rev: 6.0.1
hooks:
- id: beautysh
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black

View file

@ -1,52 +0,0 @@
language: python
python:
- '2.7'
- '3.5'
- '3.6'
before_install: sudo apt-get install unzip
before_script:
- export TFVER=0.10.0
- export TFURL=https://releases.hashicorp.com/terraform/
- TFURL+=$TFVER
- TFURL+="/terraform_"
- TFURL+=$TFVER
- TFURL+="_linux_amd64.zip"
- wget $TFURL -O terraform_bin.zip
- mkdir tf_bin
- unzip terraform_bin.zip -d tf_bin
install:
- curl https://bootstrap.pypa.io/ez_setup.py -o - | python
- pip install tox-travis
- pip install .
script:
- export PATH=$PATH:$PWD/tf_bin
- tox
branches:
only:
- master
- develop
- release/**
deploy:
- provider: pypi
distributions: sdist
server: https://testpypi.python.org/pypi
user: beelit94
password:
secure: sWxc+p/gdq3k2WbUGNG2F4TukFNkTkvq6OPaiOvyfgWThYNk6/juRkMd8flmTbh0VGhcjFbpDLeSApb2kFhfiokYJSH1hOOcmXf8xzYH8/+R4DDEiGa5Y/pR9TBvYu4S8eJEfFUFfb1BBpapykj7o43hcaqMExBJIdVJU7aeoEAC1jQeTJh8wWwdJKHy2dNSM+6RVhk3e5+b0LfK7Bk5sU5P+YdEMj79MJU450J4OmZXWzJgvBN5/2QfVa5LrUD00nYuGuiBniz2lVevIHWjUYawUzpPsTa7F0s2WemG9YcV7U8u06xNjY9Ce3CTbxNhc7OIKq+TCkOgR3qZFXVJ8A87G+AT2iQ01VslQ4DJCxnJNTnpqojWnwf6MFL9O8ONioWYO32bhQFKOQ806ASHP4lNMRDKqx8hXtP5In7/r0SARKscv6Bas83rp+FESkKD5vWgkZJG+yx96LlwRLUhSVnVyb/nOJ++zt5RR3BvY2O4p9YAZY3Qt8TQihOdBQKnY3UXsMyNaE25+yvyNWpmyJiePRbTUd+cpLnycnqG9Ll8v6TpFXb6ahFMjlAFfJNQYlREfseClTHSRjZNxfsXGQCsJh6TZAq7jOB5hCk3q41eOUFWARxbyj8j59NBV8fSQrrGJJ9/VZKQeYiQlBB9KpK4PrnH84oeQ8i+VSbVr5w=
on:
branch: release/**
tags: false
condition: $TRAVIS_PYTHON_VERSION = "3.5"
- provider: pypi
distributions: sdist
user: beelit94
password:
secure: QhCiTLrBvw/Uzt3eiLEmvMP3uHnayVCETqEDA+2+Q9vFavqj0CHA76zqYonBFqnh0a3HFCRIVVt+6ynpZ10kpQ3tAObIw+pY39ZPnpAhOjSpFzzMdpIF9Bhv9A93ng2iSESAZPAOwktHzUwjFx0Zvl0lSYD9rutHgttGgdU2CajiUtwTUhCTjOAVdR2Gm+15H808vzKWnMaKflXxZt+fkt279mQTYAtz6eBWtZwIKry/uAJCSrPSWtbi50O0HsWRMXLXWH5Jn/BVjWSDSM92DssUDq0D+tQyp4M5nQXJ9EyAvEdsKNLx3cvNruznh2ohI2jmcoIjwFiS6+wrEmUiXkP86iyzCSqL/EbcOG0xUh3vbfYtMBp7jENgD405+3SEhPY4PlqUmc+HDtB7FUcHz4y7wGWJRGyQzNnjJ6Tv0Ajdz5mfJubWVIvHjcRqkxTVtUKt50o00xZ62M0ZzQkDTIHQEsZly0XeHAgSvNzWkmjt9BiBrZ9OkoWVkRpSrCBy/EcpDNPCTSfSzOQ0Nq1ePFjkkW1n8QWDW9Pdb+/7/P2y9E2S8CT+nXBkRQeQiO86Qf1Ireg7k9TA5VYisVZ6bEXEc9UV0mAojpSsC7zWhVlbAoltN6ZbjKmqy/wqn2QIcJemcSie0JigzKpdw7l8FPT2lCRyTKlYLpRyKXzSkNI=
on:
branch: master
tags: false
condition: $TRAVIS_PYTHON_VERSION = "3.5"
notifications:
email:
recipients:
- beelit94@gmail.com

View file

@ -1,6 +1,24 @@
# Changelog # Changelog
## [0.9.1]
1. [#10] log handler error on Linux environment
1. [#11] Fix reading state file for remote state and support backend config for
init command
## [0.9.0] ## [0.9.0]
### Fixed
1. [#12] Output function doesn't accept parameter 'module' 1. [#12] Output function doesn't accept parameter 'module'
1. [#16] Handle empty space/special characters when passing string to command line options 1. [#16] Handle empty space/special characters when passing string to command line options
1. Tested with terraform 0.10.0 1. Tested with terraform 0.10.0
## [0.10.0]
1. [#27] No interaction for apply function
1. [#18] Return access to the subprocess so output can be handled as desired
1. [#24] Full support for output(); support for raise_on_error
## [0.10.1]
1. [#48] adding extension for temp file to adopt the change in terraform 0.12.0
1. [#49] add workspace support
## [1.0.1]
1. adding option to output the latest cmd
1. added refresh command
1. intenden to work with tf1.0

0
CONTRIBUTING.md Normal file
View file

View file

@ -1,3 +1,3 @@
Please see README at github_ Please see README at github_
.. _github: https://github.com/beelit94/python-terraform/blob/master/README.md .. _github: https://github.com/DomainDrivenArchitecture/python-terraform/blob/develop/README.md

View file

@ -1,18 +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
python-terraform is a python module provide a wrapper of `terraform` command line tool. dda-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/
### Status
[![Build Status](https://travis-ci.org/beelit94/python-terraform.svg?branch=develop)](https://travis-ci.org/beelit94/python-terraform)
## Installation ## Installation
pip install python-terraform pip install python-terraform
## Usage ## Usage
#### For any terraform command #### For any terraform command
from python_terraform import * from dda_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)
@ -20,13 +23,13 @@ python-terraform is a python module provide a wrapper of `terraform` command lin
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 python_terraform import * from dda_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 python_terraform import * from dda_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)
@ -79,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 python_terraform import Terraform from dda_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)
@ -93,19 +96,19 @@ In shell:
In python-terraform: In python-terraform:
from python_terraform import * from dda_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 python_terraform import * from dda_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 python_terraform import * from dda_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)
@ -117,7 +120,7 @@ In shell:
In python-terraform: In python-terraform:
from python_terraform import * from dda_python_terraform import *
tf = terraform(working_dir='/home/test') tf = terraform(working_dir='/home/test')
tf.fmt(diff=True) tf.fmt(diff=True)
@ -138,10 +141,11 @@ 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

@ -0,0 +1,9 @@
"""Module providing wrapper for terraform."""
from .terraform import (
IsFlagged,
IsNotFlagged,
Terraform,
TerraformCommandError,
VariableFiles,
)
from .tfstate import Tfstate

View file

@ -0,0 +1,578 @@
"""Module providing wrapper for terraform."""
import json
import logging
import os
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
logger = logging.getLogger(__name__)
COMMAND_WITH_SUBCOMMANDS = {"workspace"}
class TerraformFlag:
pass
class IsFlagged(TerraformFlag):
pass
class IsNotFlagged(TerraformFlag):
pass
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)
self.out = out
self.err = err
logger.error("Error with command %s. Reason: %s", self.cmd, self.err)
class Terraform:
"""Wrapper of terraform command line tool.
https://www.terraform.io/
"""
def __init__(
self,
working_dir: Optional[str] = None,
targets: Optional[Sequence[str]] = None,
state: Optional[str] = None,
variables: Optional[Dict[str, str]] = None,
parallelism: Optional[str] = None,
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"
):
"""
:param working_dir: the folder of the working folder, if not given,
will be current working folder
:param targets: list of target
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
: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.parallelism = parallelism
self.terraform_bin_path = (
terraform_bin_path if terraform_bin_path else "terraform"
)
self.terraform_semantic_version = terraform_semantic_version
self.var_file = var_file
self.temp_var_files = VariableFiles()
# store the tfstate data
self.tfstate = None
self.read_state_file(self.state)
self.latest_cmd = ''
def apply(
self,
dir_or_plan: Optional[str] = None,
input: bool = False,
skip_plan: bool = True,
no_color: Type[TerraformFlag] = IsFlagged,
**kwargs,
) -> CommandOutput:
"""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 skip_plan: force apply without plan (default: false)
:param kwargs: same as kwags in method 'cmd'
:returns return_code, stdout, stderr
"""
if not skip_plan:
return self.plan(dir_or_plan=dir_or_plan, **kwargs)
global_opts = self._generate_default_general_options(dir_or_plan)
default = kwargs.copy()
default["input"] = input
default["no_color"] = no_color
default["auto-approve"] = True # a False value will require an input
option_dict = self._generate_default_options(default)
args = self._generate_default_args(dir_or_plan)
return self.cmd(global_opts, "apply", *args, **option_dict)
def refresh(
self,
dir_or_plan: Optional[str] = None,
input: bool = False,
no_color: Type[TerraformFlag] = IsFlagged,
**kwargs,
) -> CommandOutput:
"""Refer to https://terraform.io/docs/commands/refresh.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
"""
global_opts = self._generate_default_general_options(dir_or_plan)
default = kwargs.copy()
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(global_opts, "refresh", *args, **option_dict)
def destroy(
self,
dir_or_plan: Optional[str] = None,
force: Type[TerraformFlag] = IsFlagged,
**kwargs,
) -> CommandOutput:
"""Refer to https://www.terraform.io/docs/commands/destroy.html
force/no-color option is flagged by default
:return: ret_code, stdout, stderr
"""
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"):
default["force"] = force
default["auto-approve"] = True
options = self._generate_default_options(default)
args = self._generate_default_args(dir_or_plan)
return self.cmd(global_opts, "destroy", *args, **options)
def plan(
self,
dir_or_plan: Optional[str] = None,
detailed_exitcode: Type[TerraformFlag] = IsFlagged,
**kwargs,
) -> CommandOutput:
"""Refer 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
"""
global_opts = self._generate_default_general_options(dir_or_plan)
options = kwargs.copy()
options["detailed_exitcode"] = detailed_exitcode
options = self._generate_default_options(options)
args = self._generate_default_args(dir_or_plan)
return self.cmd(global_opts, "plan", *args, **options)
def init(
self,
dir_or_plan: Optional[str] = None,
backend_config: Optional[Dict[str, str]] = None,
reconfigure: Type[TerraformFlag] = IsFlagged,
backend: bool = True,
**kwargs,
) -> CommandOutput:
"""Refer to https://www.terraform.io/docs/commands/init.html
By default, this assumes you want to use backend config, and tries to
init fresh. The flags -reconfigure and -backend=true are default.
:param dir_or_plan: relative path to the folder want to init
:param backend_config: a dictionary of backend config options. eg.
t = Terraform()
t.init(backend_config={'access_key': 'myaccesskey',
'secret_key': 'mysecretkey', 'bucket': 'mybucketname'})
:param reconfigure: whether or not to force reconfiguration of backend
:param backend: whether or not to use backend settings for init
:param kwargs: options
:return: ret_code, stdout, stderr
"""
options = kwargs.copy()
options.update(
{
"backend_config": backend_config,
"reconfigure": reconfigure,
"backend": backend,
}
)
options = self._generate_default_options(options)
global_opts = self._generate_default_general_options(dir_or_plan)
args = self._generate_default_args(dir_or_plan)
return self.cmd(global_opts, "init", *args, **options)
def generate_cmd_string(self, global_options: Dict[str, Any], cmd: str, *args, **kwargs) -> List[str]:
"""For any generate_cmd_string doesn't written as public method of Terraform
examples:
1. call import command,
ref to https://www.terraform.io/docs/commands/import.html
--> generate_cmd_string call:
terraform import -input=true aws_instance.foo i-abcd1234
--> python call:
tf.generate_cmd_string('import', 'aws_instance.foo', 'i-abcd1234', input=True)
2. call apply command,
--> generate_cmd_string call:
terraform apply -var='a=b' -var='c=d' -no-color the_folder
--> python call:
tf.generate_cmd_string('apply', the_folder, no_color=IsFlagged, var={'a':'b', 'c':'d'})
:param cmd: command and sub-command of terraform, seperated with space
refer to https://www.terraform.io/docs/commands/index.html
:param args: arguments of a command
:param kwargs: same as kwags in method 'cmd'
:return: string of valid terraform command
"""
cmds = [self.terraform_bin_path]
cmds += self._generate_cmd_options(**global_options)
cmds += cmd.split()
if cmd in COMMAND_WITH_SUBCOMMANDS:
args = list(args)
subcommand = args.pop(0)
cmds.append(subcommand)
cmds += self._generate_cmd_options(**kwargs)
cmds += args
self.latest_cmd = ' '.join(cmds)
return cmds
def cmd(
self,
global_opts: Dict[str, Any],
cmd: str,
*args,
capture_output: Union[bool, str] = True,
raise_on_error: bool = True,
synchronous: bool = True,
**kwargs,
) -> CommandOutput:
"""Run a terraform command, if success, will try to read state file
:param cmd: command and sub-command of terraform, seperated with space
refer to https://www.terraform.io/docs/commands/index.html
:param args: arguments of a command
:param kwargs: any option flag with key value without prefixed dash character
if there's a dash in the option name, use under line instead of dash,
ex. -no-color --> no_color
if it's a simple flag with no value, value should be IsFlagged
ex. cmd('taint', allow_missing=IsFlagged)
if it's a boolean value flag, assign True or false
if it's a flag could be used multiple times, assign list to it's value
if it's a "var" variable flag, assign dictionary to it
if a value is None, will skip this option
if the option 'capture_output' is passed (with any value other than
True), terraform output will be printed to stdout/stderr and
"None" will be returned as out and err.
if the option 'raise_on_error' is passed (with any value that evaluates to True),
and the terraform command returns a nonzerop return code, then
a TerraformCommandError exception will be raised. The exception object will
have the following properties:
returncode: The command's return code
out: The captured stdout, or None if not captured
err: The captured stderr, or None if not captured
:return: ret_code, out, err
"""
if capture_output is True:
stderr = subprocess.PIPE
stdout = subprocess.PIPE
elif capture_output == "framework":
stderr = None
stdout = None
else:
stderr = sys.stderr
stdout = sys.stdout
cmds = self.generate_cmd_string(global_opts, cmd, *args, **kwargs)
logger.info("Command: %s", " ".join(cmds))
working_folder = self.working_dir if self.working_dir else None
environ_vars = {}
if self.is_env_vars_included:
environ_vars = os.environ.copy()
proc = 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
logger.info("output: %s", out)
if ret_code == 0:
self.read_state_file()
else:
logger.warning("error: %s", err)
self.temp_var_files.clean_up()
if capture_output is True:
out = out.decode()
err = err.decode()
else:
out = None
err = None
if ret_code and raise_on_error:
raise TerraformCommandError(ret_code, " ".join(cmds), out=out, err=err)
return ret_code, out, err
def output(
self, dir_or_plan: Optional[str] = None, *args, capture_output: bool = True, **kwargs
) -> Union[None, str, Dict[str, str], Dict[str, Dict[str, str]]]:
"""Refer https://www.terraform.io/docs/commands/output.html
Note that this method does not conform to the (ret_code, out, err) return
convention. To use the "output" command with the standard convention,
call "output_cmd" instead of "output".
:param args: Positional arguments. There is one optional positional
argument NAME; if supplied, the returned output text
will be the json for a single named output value.
:param kwargs: Named options, passed to the command. In addition,
'full_value': If True, and NAME is provided, then
the return value will be a dict with
"value', 'type', and 'sensitive'
properties.
:return: None, if an error occured
Output value as a string, if NAME is provided and full_value
is False or not provided
Output value as a dict with 'value', 'sensitive', and 'type' if
NAME is provided and full_value is True.
dict of named dicts each with 'value', 'sensitive', and 'type',
if NAME is not provided
"""
kwargs["json"] = IsFlagged
if capture_output is False:
raise ValueError("capture_output is required for this method")
global_opts = self._generate_default_general_options(dir_or_plan)
ret, out, _ = self.cmd(global_opts, "output", *args, **kwargs)
# ret, out, _ = self.output_cmd(global_opts, *args, **kwargs)
if ret:
return None
return json.loads(out.lstrip())
def read_state_file(self, file_path=None) -> None:
"""Read .tfstate file
:param file_path: relative path to working dir
:return: states file in dict type
"""
working_dir = self.working_dir or ""
file_path = file_path or self.state or ""
if not file_path:
backend_path = os.path.join(file_path, ".terraform", "terraform.tfstate")
if os.path.exists(os.path.join(working_dir, backend_path)):
file_path = backend_path
else:
file_path = os.path.join(file_path, "terraform.tfstate")
file_path = os.path.join(working_dir, file_path)
self.tfstate = Tfstate.load_file(file_path)
def set_workspace(self, workspace, *args, **kwargs) -> CommandOutput:
"""Set workspace
:param workspace: the desired workspace.
:return: status
"""
global_opts = self._generate_default_general_options(False)
return self.cmd(global_opts, "workspace", "select", workspace, *args, **kwargs)
def create_workspace(self, workspace, *args, **kwargs) -> CommandOutput:
"""Create workspace
:param workspace: the desired workspace.
:return: status
"""
global_opts = self._generate_default_general_options(False)
return self.cmd(global_opts, "workspace", "new", workspace, *args, **kwargs)
def delete_workspace(self, workspace, *args, **kwargs) -> CommandOutput:
"""Delete workspace
:param workspace: the desired workspace.
:return: status
"""
global_opts = self._generate_default_general_options(False)
return self.cmd(global_opts, "workspace", "delete", workspace, *args, **kwargs)
def show_workspace(self, **kwargs) -> CommandOutput:
"""Show workspace, this command does not need the [DIR] part
:return: workspace
"""
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):
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):
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 {}
def _generate_default_options(
self, input_options: Dict[str, Any]
) -> Dict[str, Any]:
return {
"state": self.state,
"target": self.targets,
"var": self.variables,
"var_file": self.var_file,
"parallelism": self.parallelism,
"no_color": IsFlagged,
"input": False,
**input_options,
}
def _generate_cmd_options(self, **kwargs) -> List[str]:
result = []
for option, value in kwargs.items():
if "_" in option:
option = option.replace("_", "-")
if isinstance(value, list):
for sub_v in value:
result += [f"-{option}={sub_v}"]
continue
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}"]
continue
# since map type sent in string won't work, create temp var file for
# variables, and clean it up later
if 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:
filename = self.temp_var_files.create(value)
result += [f"-var-file={filename}"]
continue
# simple flag,
if value is IsFlagged:
result += [f"-{option}"]
continue
if value is None or value is IsNotFlagged:
continue
if isinstance(value, bool):
value = "true" if value else "false"
result += [f"-{option}={value}"]
return result
def __exit__(self, exc_type, exc_value, traceback) -> None:
self.temp_var_files.clean_up()
def __getattr__(self, item: str) -> Callable:
def wrapper(*args, **kwargs):
cmd_name = str(item)
if cmd_name.endswith("_cmd"):
cmd_name = cmd_name[:-4]
logger.debug("called with %r and %r", args, kwargs)
global_opts = self._generate_default_general_options(False)
return self.cmd(global_opts, cmd_name, *args, **kwargs)
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:
logger.debug("%s is created", temp.name)
self.files.append(temp)
logger.debug("variables wrote to tempfile: %s", variables)
temp.write(json.dumps(variables))
file_name = temp.name
return file_name
def clean_up(self):
"""cleanup the var file"""
for fle in self.files:
os.unlink(fle.name)
self.files = []

View file

@ -0,0 +1,35 @@
"""Helper Module providing wrapper for terraform state."""
import json
import logging
import os
from typing import Dict, Optional
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
if data:
self.__dict__ = data
@staticmethod
def load_file(file_path: str) -> "Tfstate":
"""Read the tfstate file and load its contents.
Parses then as JSON and put the result into the object.
"""
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)
tf_state = Tfstate(json_data)
tf_state.tfstate_file = file_path
return tf_state
logger.debug("%s does not exist", file_path)
return Tfstate()

12
doc/releasing.md Normal file
View file

@ -0,0 +1,12 @@
## 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,310 +0,0 @@
# -*- coding: utf-8 -*-
# above is for compatibility of python2.7.11
import subprocess
import os
import sys
import json
import logging
import tempfile
from python_terraform.tfstate import Tfstate
log = logging.getLogger(__name__)
class IsFlagged:
pass
class IsNotFlagged:
pass
class Terraform(object):
"""
Wrapper of terraform command line tool
https://www.terraform.io/
"""
def __init__(self, working_dir=None,
targets=None,
state=None,
variables=None,
parallelism=None,
var_file=None,
terraform_bin_path=None,
is_env_vars_included=True):
"""
:param working_dir: the folder of the working folder, if not given,
will be current working folder
:param targets: list of target
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
:type is_env_vars_included: bool
:param is_env_vars_included: included env variables when calling terraform cmd
"""
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 = dict() if variables is None else variables
self.parallelism = parallelism
self.terraform_bin_path = terraform_bin_path \
if terraform_bin_path else 'terraform'
self.var_file = var_file
self.temp_var_files = VariableFiles()
# store the tfstate data
self.tfstate = None
self.read_state_file(self.state)
def __getattr__(self, item):
def wrapper(*args, **kwargs):
cmd_name = str(item)
if cmd_name.endswith('_cmd'):
cmd_name = cmd_name[:-4]
logging.debug('called with %r and %r' % (args, kwargs))
return self.cmd(cmd_name, *args, **kwargs)
return wrapper
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 = 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 _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
option_dict['var_file'] = self.var_file
option_dict['parallelism'] = self.parallelism
option_dict['no_color'] = IsFlagged
option_dict['input'] = False
option_dict.update(input_options)
return option_dict
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 = 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):
"""
for any generate_cmd_string doesn't written as public method of terraform
examples:
1. call import command,
ref to https://www.terraform.io/docs/commands/import.html
--> generate_cmd_string call:
terraform import -input=true aws_instance.foo i-abcd1234
--> python call:
tf.generate_cmd_string('import', 'aws_instance.foo', 'i-abcd1234', input=True)
2. call apply command,
--> generate_cmd_string call:
terraform apply -var='a=b' -var='c=d' -no-color the_folder
--> python call:
tf.generate_cmd_string('apply', the_folder, no_color=IsFlagged, var={'a':'b', 'c':'d'})
:param cmd: command and sub-command of terraform, seperated with space
refer to https://www.terraform.io/docs/commands/index.html
:param args: arguments of a command
:param kwargs: same as kwags in method 'cmd'
:return: string of valid terraform command
"""
cmds = cmd.split()
cmds = [self.terraform_bin_path] + cmds
for k, v in kwargs.items():
if '_' in k:
k = k.replace('_', '-')
if type(v) is list:
for sub_v in v:
cmds += ['-{k}={v}'.format(k=k, v=sub_v)]
continue
# right now we assume only variables will be passed as dict
# since map type sent in string won't work, create temp var file for
# variables, and clean it up later
if type(v) is dict:
filename = self.temp_var_files.create(v)
cmds += ['-var-file={0}'.format(filename)]
continue
# simple flag,
if v is IsFlagged:
cmds += ['-{k}'.format(k=k)]
continue
if v is None or v is IsNotFlagged:
continue
if type(v) is bool:
v = 'true' if v else 'false'
cmds += ['-{k}={v}'.format(k=k, v=v)]
cmds += args
return cmds
def cmd(self, cmd, *args, **kwargs):
"""
run a terraform command, if success, will try to read state file
:param cmd: command and sub-command of terraform, seperated with space
refer to https://www.terraform.io/docs/commands/index.html
:param args: arguments of a command
:param kwargs: any option flag with key value without prefixed dash character
if there's a dash in the option name, use under line instead of dash,
ex. -no-color --> no_color
if it's a simple flag with no value, value should be IsFlagged
ex. cmd('taint', allow_missing=IsFlagged)
if it's a boolean value flag, assign True or false
if it's a flag could be used multiple times, assign list to it's value
if it's a "var" variable flag, assign dictionary to it
if a value is None, will skip this option
if the option 'capture_output' is passed (with any value other than
True), terraform output will be printed to stdout/stderr and
"None" will be returned as out and err.
:return: ret_code, out, err
"""
capture_output = kwargs.pop('capture_output', True)
if capture_output is True:
stderr = subprocess.PIPE
stdout = subprocess.PIPE
else:
stderr = sys.stderr
stdout = sys.stdout
cmds = self.generate_cmd_string(cmd, *args, **kwargs)
log.debug('command: {c}'.format(c=' '.join(cmds)))
working_folder = self.working_dir if self.working_dir else None
environ_vars = {}
if self.is_env_vars_included:
environ_vars = os.environ.copy()
p = subprocess.Popen(cmds, stdout=stdout, stderr=stderr,
cwd=working_folder, env=environ_vars)
out, err = p.communicate()
ret_code = p.returncode
log.debug('output: {o}'.format(o=out))
if ret_code == 0:
self.read_state_file()
else:
log.warn('error: {e}'.format(e=err))
self.temp_var_files.clean_up()
if capture_output is True:
return ret_code, out.decode('utf-8'), err.decode('utf-8')
else:
return ret_code, None, None
def output(self, name, *args, **kwargs):
"""
https://www.terraform.io/docs/commands/output.html
:param name: name of output
:return: output value
"""
ret, out, err = self.cmd(
'output', name, json=IsFlagged, *args, **kwargs)
log.debug('output raw string: {0}'.format(out))
if ret != 0:
return None
out = out.lstrip()
output_dict = json.loads(out)
return output_dict['value']
def read_state_file(self, file_path=None):
"""
read .tfstate file
:param file_path: relative path to working dir
:return: states file in dict type
"""
if not file_path:
file_path = self.state
if not file_path:
file_path = 'terraform.tfstate'
if self.working_dir:
file_path = os.path.join(self.working_dir, file_path)
self.tfstate = Tfstate.load_file(file_path)
def __exit__(self, exc_type, exc_value, traceback):
self.temp_var_files.clean_up()
class VariableFiles(object):
def __init__(self):
self.files = []
def create(self, variables):
with tempfile.NamedTemporaryFile('w+t', delete=False) as temp:
log.debug('{0} is created'.format(temp.name))
self.files.append(temp)
log.debug('variables wrote to tempfile: {0}'.format(str(variables)))
temp.write(json.dumps(variables))
file_name = temp.name
return file_name
def clean_up(self):
for f in self.files:
os.unlink(f.name)
self.files = []

View file

@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
# above is for compatibility of python2.7.11
import json
import os
import logging
log = logging.getLogger(__name__)
class Tfstate(object):
def __init__(self, data=None):
self.tfstate_file = None
self.native_data = data
if data:
self.__dict__ = data
@staticmethod
def load_file(file_path):
"""
Read the tfstate file and load its contents, parses then as JSON and put the result into the object
"""
log.debug('read data from {0}'.format(file_path))
if os.path.exists(file_path):
with open(file_path) as f:
json_data = json.load(f)
tf_state = Tfstate(json_data)
tf_state.tfstate_file = file_path
return tf_state
log.debug('{0} is not exist'.format(file_path))
return Tfstate()

View file

@ -1,3 +1 @@
tox-pyenv packaging
pytest
tox

11
requirements_dev.txt Normal file
View file

@ -0,0 +1,11 @@
coverage==5.3
flake8==3.8.4
flake8-polyfill==1.0.2
mypy==0.790
mypy-extensions==0.4.3
pycodestyle==2.6.0
pyflakes==2.2.0
pylint==2.6.0
pytest==6.1.2
pytest-cov==2.10.1
pytest-datafiles==2.0

View file

@ -1,2 +1,10 @@
[wheel] [wheel]
universal = 1 universal = 1
[isort]
line_length=88
known_third_party=
indent=' '
multi_line_output=3
sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
include_trailing_comma=true

View file

@ -7,12 +7,13 @@ except ImportError:
from distutils.core import setup from distutils.core import setup
dependencies = [] dependencies = []
module_name = 'python-terraform' module_name = "dda-python-terraform"
short_description = 'This is a python module provide a wrapper ' \ short_description = (
'of terraform command line tool' "This is a python module provide a wrapper " "of terraform command line tool"
)
try: try:
with open('DESCRIPTION.rst') as f: with open("DESCRIPTION.rst") as f:
long_description = f.read() long_description = f.read()
except IOError: except IOError:
long_description = short_description long_description = short_description
@ -20,36 +21,37 @@ except IOError:
setup( setup(
name=module_name, name=module_name,
version='0.9.0', version="2.1.2-dev",
url='https://github.com/beelit94/python-terraform', url="https://repo.prod.meissa.de/meissa/dda-python-terraform",
license='MIT', license="MIT",
author='Freddy Tan', author="Freddy Tan, meissa team",
author_email='beelit94@gmail.com', author_email="buero@meissa.de",
description=short_description, description=short_description,
long_description=long_description, long_description=long_description,
packages=['python_terraform'], packages=["dda_python_terraform"],
package_data={}, package_data={},
platforms='any', platforms="any",
install_requires=dependencies, install_requires=dependencies,
tests_require=["pytest"],
python_requires=">=3.6",
classifiers=[ classifiers=[
# As from http://pypi.python.org/pypi?%3Aaction=list_classifiers # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers
# 'Development Status :: 1 - Planning', # 'Development Status :: 1 - Planning',
# 'Development Status :: 2 - Pre-Alpha', # 'Development Status :: 2 - Pre-Alpha',
# 'Development Status :: 3 - Alpha', # 'Development Status :: 3 - Alpha',
'Development Status :: 4 - Beta', "Development Status :: 4 - Beta",
# 'Development Status :: 5 - Production/Stable', # 'Development Status :: 5 - Production/Stable',
# 'Development Status :: 6 - Mature', # 'Development Status :: 6 - Mature',
# 'Development Status :: 7 - Inactive', # 'Development Status :: 7 - Inactive',
'Environment :: Console', "Environment :: Console",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'License :: OSI Approved :: MIT License', "License :: OSI Approved :: MIT License",
'Operating System :: POSIX', "Operating System :: POSIX",
'Operating System :: MacOS', "Operating System :: MacOS",
'Operating System :: Unix', "Operating System :: Unix",
# 'Operating System :: Windows', # 'Operating System :: Windows',
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 2', "Programming Language :: Python :: 3",
'Programming Language :: Python :: 3', "Topic :: Software Development :: Libraries :: Python Modules",
'Topic :: Software Development :: Libraries :: Python Modules', ],
]
) )

View file

@ -5,12 +5,12 @@ variable "test_var" {
provider "archive" {} provider "archive" {}
variable "test_list_var" { variable "test_list_var" {
type = "list" type = list(string)
default = ["a", "b"] default = ["a", "b"]
} }
variable "test_map_var" { variable "test_map_var" {
type = "map" type = map
default = { default = {
"a" = "a" "a" = "a"
@ -19,13 +19,13 @@ variable "test_map_var" {
} }
output "test_output" { output "test_output" {
value = "${var.test_var}" value = var.test_var
} }
output "test_list_output" { output "test_list_output" {
value = "${var.test_list_var}" value = var.test_list_var
} }
output "test_map_output" { output "test_map_output" {
value = "${var.test_map_var}" value = var.test_map_var
} }

View file

@ -1,70 +1,209 @@
try: import fnmatch
from cStringIO import StringIO # Python 2
except ImportError:
from io import StringIO
from python_terraform import *
import pytest
import os
import logging import logging
import os
import re import re
import shutil import shutil
import fnmatch 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
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
root_logger = logging.getLogger() root_logger = logging.getLogger()
current_path = os.path.dirname(os.path.realpath(__file__)) current_path = os.path.dirname(os.path.realpath(__file__))
semantic_version = os.environ.get("TFVER")
FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS = "test 'test.out!" FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS = "test 'test.out!"
STRING_CASES = [ STRING_CASES = [
[ [
lambda x: x.generate_cmd_string('apply', 'the_folder', lambda x: x.generate_cmd_string(
no_color=IsFlagged), {}, "apply", "the_folder", no_color=IsFlagged),
"terraform apply -no-color the_folder" "terraform apply -no-color the_folder",
], ],
[ [
lambda x: x.generate_cmd_string('push', 'path', vcs=True, lambda x: x.generate_cmd_string({},
token='token', "push", "path", vcs=True, token="token", atlas_address="url"
atlas_address='url'), ),
"terraform push -vcs=true -token=token -atlas-address=url path" "terraform push -vcs=true -token=token -atlas-address=url path",
], ],
] [
lambda x: x.generate_cmd_string({},
"refresh", "path", token="token"
),
"terraform refresh -token=token path",
],
]
CMD_CASES = [ CMD_CASES_0_x = [
['method', 'expected_output', 'expected_ret_code', 'expected_logs', 'folder'], [
"method",
"expected_output",
"expected_ret_code",
"expected_exception",
"expected_logs",
"folder",
],
[ [
[ [
lambda x: x.cmd('plan', 'var_to_output', no_color=IsFlagged, var={'test_var': 'test'}) , lambda x: x.cmd(
"doesn't need to do anything", {},
"plan",
"var_to_output",
no_color=IsFlagged,
var={"test_var": "test"},
raise_on_error=False,
),
# Expected output varies by terraform semantic_version
"Plan: 0 to add, 0 to change, 0 to destroy.",
0, 0,
'', False,
'var_to_output' "",
"var_to_output",
], ],
# try import aws instance # try import aws instance
[ [
lambda x: x.cmd('import', 'aws_instance.foo', 'i-abcd1234', no_color=IsFlagged), lambda x: x.cmd(
'', {},
"import",
"aws_instance.foo",
"i-abcd1234",
no_color=IsFlagged,
raise_on_error=False,
),
"",
1, 1,
'command: terraform import -no-color aws_instance.foo i-abcd1234', False,
'' "Error: No Terraform configuration files",
"",
], ],
# test with space and special character in file path # test with space and special character in file path
[ [
lambda x: x.cmd('plan', 'var_to_output', out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS), lambda x: x.cmd(
'', {},
"plan",
"var_to_output",
out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS,
raise_on_error=False,
),
"",
0, 0,
'', False,
'var_to_output' "",
] "var_to_output",
] ],
# test workspace command (commands with subcommand)
[
lambda x: x.cmd(
{}, "workspace", "show", no_color=IsFlagged, raise_on_error=False
),
"",
0,
False,
"Command: terraform workspace show -no-color",
"",
],
],
] ]
@pytest.fixture(scope='function') CMD_CASES_1_x = [
[
"method",
"expected_output",
"expected_ret_code",
"expected_exception",
"expected_logs",
"folder",
],
[
[
lambda x: x.cmd(
{"chdir": "var_to_output"},
"plan",
no_color=IsFlagged,
var={"test_var": "test"},
raise_on_error=False,
),
# Expected output varies by terraform semantic_version
"Changes to Outputs:",
0,
False,
"",
"var_to_output",
],
# try import aws instance
[
lambda x: x.cmd(
{},
"import",
"aws_instance.foo",
"i-abcd1234",
no_color=IsFlagged,
raise_on_error=False,
),
"",
1,
False,
"Error: No Terraform configuration files",
"",
],
# test with space and special character in file path
[
lambda x: x.cmd(
{"chdir": "var_to_output"},
"plan",
out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS,
raise_on_error=False,
),
"",
0,
False,
"",
"var_to_output",
],
# test workspace command (commands with subcommand)
[
lambda x: x.cmd(
{}, "workspace", "show", no_color=IsFlagged, raise_on_error=False
),
"",
0,
False,
"Command: terraform workspace show -no-color",
"",
],
],
]
APPLY_CASES_0_x = [
["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={"c"="c""d"="d"}', {},),
("var_to_output", {"test_map_var": {"c": "c", "d": "d"}}, "var_to_output/test_map_var.json", 'test_map_output={"e"="e""f"="f"}', {},),
("var_to_output", {}, None, "\x1b[0m\x1b[1m\x1b[32mApplycomplete!", {"no_color": IsNotFlagged},), ]]
APPLY_CASES_1_x = [
["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=tolist(["c","d",])', {},),
("var_to_output", {"test_map_var": {"c": "c", "d": "d"}}, None, 'test_map_output=tomap({"c"="c""d"="d"})', {},),
("var_to_output", {"test_map_var": {"c": "c", "d": "d"}}, "test_map_var.json", 'test_map_output=tomap({"e"="e""f"="f"})', {},),
("var_to_output", {}, None, "\x1b[0m\x1b[1m\x1b[32mApplycomplete!", {"no_color": IsNotFlagged},), ]]
@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")
shutil.copy(orgin, shutil.copy(orgin, target)
target)
def td(): def td():
shutil.move(target, orgin) shutil.move(target, orgin)
@ -73,28 +212,51 @@ def fmt_test_file(request):
return return
# @pytest.fixture()
# def string_logger(request) -> Callable[..., str]:
# log_stream = StringIO()
# handler = logging.StreamHandler(log_stream)
# root_logger.addHandler(handler)
# def td():
# root_logger.removeHandler(handler)
# log_stream.close()
# request.addfinalizer(td)
# return lambda: str(log_stream.getvalue())
@pytest.fixture() @pytest.fixture()
def string_logger(request): def workspace_setup_teardown():
log_stream = StringIO() """Fixture used in workspace related tests.
handler = logging.StreamHandler(log_stream)
root_logger.addHandler(handler)
def td(): Create and tear down a workspace
root_logger.removeHandler(handler) *Use as a contextmanager*
log_stream.close() """
request.addfinalizer(td) @contextmanager
return lambda: str(log_stream.getvalue()) def wrapper(workspace_name, create=True, delete=True, *args, **kwargs):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf.init()
if create:
tf.create_workspace(workspace_name, *args, **kwargs)
yield tf
if delete:
tf.set_workspace("default")
tf.delete_workspace(workspace_name)
yield wrapper
class TestTerraform(object): class TestTerraform:
def teardown_method(self, method): def teardown_method(self, _) -> None:
""" teardown any state that was previously setup with a setup_method """Teardown any state that was previously setup with a setup_method call."""
call. exclude = ["test_tfstate_file",
""" "test_tfstate_file2", "test_tfstate_file3"]
def purge(dir, pattern): def purge(dir: str, pattern: str) -> None:
for root, dirnames, filenames in os.walk(dir): for root, dirnames, filenames in os.walk(dir):
dirnames[:] = [d for d in dirnames if d not in exclude]
for filename in fnmatch.filter(filenames, pattern): for filename in fnmatch.filter(filenames, pattern):
f = os.path.join(root, filename) f = os.path.join(root, filename)
os.remove(f) os.remove(f)
@ -102,134 +264,294 @@ class TestTerraform(object):
d = os.path.join(root, dirname) d = os.path.join(root, dirname)
shutil.rmtree(d) shutil.rmtree(d)
purge('.', '*.tfstate') purge(".", "*.tfstate")
purge('.', '*.terraform') purge(".", "*.tfstate.backup")
purge('.', FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS) purge(".", "*.terraform")
purge(".", FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS)
@pytest.mark.parametrize([ @pytest.mark.parametrize(["method", "expected"], STRING_CASES)
"method", "expected" def test_generate_cmd_string(self, method: Callable[..., str], expected: str):
], STRING_CASES) tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
def test_generate_cmd_string(self, method, expected):
tf = Terraform(working_dir=current_path)
result = method(tf) result = method(tf)
strs = expected.split() strs = expected.split()
for s in strs: for s in strs:
assert s in result assert s in result
@pytest.mark.parametrize(*CMD_CASES) @pytest.mark.parametrize(*(CMD_CASES_1_x if version.parse(semantic_version) >= version.parse("1.0.0") else CMD_CASES_0_x))
def test_cmd(self, method, expected_output, expected_ret_code, expected_logs, string_logger, folder): def test_cmd(
tf = Terraform(working_dir=current_path) self,
tf.init(folder) method: Callable[..., str],
ret, out, err = method(tf) expected_output: str,
logs = string_logger() expected_ret_code: int,
logs = logs.replace('\n', '') expected_exception: bool,
expected_logs: str,
caplog: LogCaptureFixture,
folder: str,
):
with caplog.at_level(logging.INFO):
tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
tf.init(folder)
try:
ret, out, _ = method(tf)
assert not expected_exception
except TerraformCommandError as e:
assert expected_exception
ret = e.returncode
out = e.out
assert expected_output in out assert expected_output in out
assert expected_ret_code == ret assert expected_ret_code == ret
assert expected_logs in logs assert expected_logs in caplog.text
@pytest.mark.parametrize(
("folder", "variables", "var_files", "expected_output", "options"), @pytest.mark.parametrize(*(APPLY_CASES_1_x if version.parse(semantic_version) >= version.parse("1.0.0") else APPLY_CASES_0_x))
[
("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, options): 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(
# after 0.10.0 we always need to init working_dir=current_path, variables=variables, var_file=var_files, terraform_semantic_version=semantic_version
)
tf.init(folder) tf.init(folder)
ret, out, err = tf.apply(folder, **options) 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 == ""
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)
folder = "var_to_output"
tf.init(folder)
tf.apply(
folder,
var_file=os.path.join(
current_path, "tfvar_files", "test.tfvars"),
)
for log in caplog.messages:
if log.startswith("Command: terraform apply"):
assert log.count("-var-file=") == 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
['cmd', 'args', 'options'], ["cmd", "args", "options"],
[ [
# bool value # bool value
('fmt', ['bad_fmt'], {'list': False, 'diff': False}) ("fmt", ["bad_fmt"], {"list": False, "diff": False})
] ],
) )
def test_options(self, cmd, args, options, fmt_test_file): def test_options(self, cmd, args, options, fmt_test_file):
tf = Terraform(working_dir=current_path) tf = Terraform(working_dir=current_path, terraform_semantic_version=semantic_version)
ret, out, err = getattr(tf, cmd)(*args, **options) ret, out, err = getattr(tf, cmd)(*args, **options)
assert ret == 0 assert ret == 0
assert out == '' assert out == ""
def test_state_data(self): def test_state_data(self):
cwd = os.path.join(current_path, 'test_tfstate_file') cwd = os.path.join(current_path, "test_tfstate_file")
tf = Terraform(working_dir=cwd, state='tfstate.test') tf = Terraform(working_dir=cwd, state="tfstate.test",
terraform_semantic_version=semantic_version)
tf.read_state_file() tf.read_state_file()
assert tf.tfstate.modules[0]['path'] == ['root'] 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.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.read_state_file()
assert tf.tfstate.modules[0]["path"] == ["default_backend"]
def test_pre_load_state_data(self): def test_pre_load_state_data(self):
cwd = os.path.join(current_path, 'test_tfstate_file') cwd = os.path.join(current_path, "test_tfstate_file")
tf = Terraform(working_dir=cwd, state='tfstate.test') tf = Terraform(working_dir=cwd, state="tfstate.test",
assert tf.tfstate.modules[0]['path'] == ['root'] terraform_semantic_version=semantic_version)
assert tf.tfstate.modules[0]["path"] == ["root"]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("folder", 'variables'), ("folder", "variables"), [("var_to_output", {"test_var": "test"})]
[
("var_to_output", {'test_var': 'test'})
]
) )
def test_override_default(self, folder, variables): def test_override_default(self, folder, variables):
tf = Terraform(working_dir=current_path, variables=variables) tf = Terraform(working_dir=current_path,
variables=variables, terraform_semantic_version=semantic_version)
tf.init(folder) tf.init(folder)
ret, out, err = tf.apply(folder, var={'test_var': 'test2'}, ret, out, err = tf.apply(
no_color=IsNotFlagged) folder, var={"test_var": "test2"}, no_color=IsNotFlagged,
out = out.replace('\n', '') )
assert '\x1b[0m\x1b[1m\x1b[32mApply' in out out = out.replace("\n", "")
out = tf.output('test_output') assert "\x1b[0m\x1b[1m\x1b[32mApply" in out
assert 'test2' in out out = tf.output(folder, "test_output")
assert "test2" in out
@pytest.mark.parametrize( @pytest.mark.parametrize("output_all", [True, False])
("param"), def test_output(self, caplog: LogCaptureFixture, output_all: bool):
[ expected_value = "test"
({}), required_output = "test_output"
({'module': 'test2'}), with caplog.at_level(logging.INFO):
] tf = Terraform(
) working_dir=current_path, variables={"test_var": expected_value}, terraform_semantic_version=semantic_version
def test_output(self, param, string_logger): )
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) tf.init("var_to_output")
tf.init('var_to_output') tf.apply("var_to_output")
tf.apply('var_to_output') params = tuple() if output_all else (required_output,)
result = tf.output('test_output', **param) result = tf.output("var_to_output", *params)
regex = re.compile("terraform output (-module=test2 -json|-json -module=test2) test_output") if output_all:
log_str = string_logger() assert result[required_output]["value"] == expected_value
if param:
assert re.search(regex, log_str), log_str
else: else:
assert result == 'test' assert result == expected_value
assert expected_value in caplog.messages[-1]
def test_destroy(self): def test_destroy(self):
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) tf = Terraform(working_dir=current_path, variables={
tf.init('var_to_output') "test_var": "test"}, terraform_semantic_version=semantic_version)
ret, out, err = tf.destroy('var_to_output') tf.init("var_to_output")
ret, out, err = tf.destroy("var_to_output")
assert ret == 0 assert ret == 0
assert 'Destroy complete! Resources: 0 destroyed.' in out assert "Destroy complete! Resources: 0 destroyed." in out
@pytest.mark.parametrize( @pytest.mark.parametrize(
("plan", "variables", "expected_ret"), ("plan", "variables", "expected_ret"), [("vars_require_input", {}, 1)]
[
('vars_require_input', {}, 1)
]
) )
def test_plan(self, plan, variables, expected_ret): def test_plan(self, plan, variables, expected_ret):
tf = Terraform(working_dir=current_path, variables=variables) tf = Terraform(working_dir=current_path,
ret, out, err = tf.plan(plan) variables=variables, terraform_semantic_version=semantic_version)
assert ret == expected_ret tf.init(plan)
with pytest.raises(TerraformCommandError) as e:
tf.plan(plan)
assert (
"\nError:" in e.value.err
)
def test_fmt(self, fmt_test_file): 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"}, terraform_semantic_version=semantic_version)
ret, out, err = tf.fmt(diff=True) ret, out, err = tf.fmt(diff=True)
assert ret == 0 assert ret == 0
def test_import(self, string_logger): def test_create_workspace(self, workspace_setup_teardown):
workspace_name = "test"
with workspace_setup_teardown(workspace_name, create=False) as tf:
ret, out, err = tf.create_workspace("test")
assert ret == 0
assert err == ""
# The directory flag is no longer supported in v1.0.8
# this should either be done with -chdir or probably just be removed
"""
def test_create_workspace_with_args(self, workspace_setup_teardown, caplog):
workspace_name = "test"
state_file_path = os.path.join(
current_path, "test_tfstate_file2", "terraform.tfstate"
)
with workspace_setup_teardown(
workspace_name, create=False
) as tf, caplog.at_level(logging.INFO):
ret, out, err = tf.create_workspace(
"test", current_path, no_color=IsFlagged
)
assert ret == 0
assert err == ""
assert (
f"Command: terraform workspace new -no-color test {current_path}"
in caplog.messages
)
"""
def test_set_workspace(self, workspace_setup_teardown):
workspace_name = "test"
with workspace_setup_teardown(workspace_name) as tf:
ret, out, err = tf.set_workspace(workspace_name)
assert ret == 0
assert err == ""
# see comment on test_create_workspace_with_args
"""
def test_set_workspace_with_args(self, workspace_setup_teardown, caplog):
workspace_name = "test"
with workspace_setup_teardown(workspace_name) as tf, caplog.at_level(
logging.INFO
):
ret, out, err = tf.set_workspace(
workspace_name, current_path, no_color=IsFlagged
)
assert ret == 0
assert err == ""
assert (
f"Command: terraform workspace select -no-color test {current_path}"
in caplog.messages
)
"""
def test_show_workspace(self, workspace_setup_teardown):
workspace_name = "test"
with workspace_setup_teardown(workspace_name) as tf:
ret, out, err = tf.show_workspace()
assert ret == 0
assert err == ""
def test_show_workspace_with_no_color(self, workspace_setup_teardown, caplog):
workspace_name = "test"
with workspace_setup_teardown(workspace_name) as tf, caplog.at_level(
logging.INFO
):
ret, out, err = tf.show_workspace(no_color=IsFlagged)
assert ret == 0
assert err == ""
assert "Command: terraform workspace show -no-color" in caplog.messages
def test_delete_workspace(self, workspace_setup_teardown):
workspace_name = "test"
with workspace_setup_teardown(workspace_name, delete=False) as tf:
tf.set_workspace("default")
ret, out, err = tf.delete_workspace(workspace_name)
assert ret == 0
assert err == ""
# see above comments
"""
def test_delete_workspace_with_args(self, workspace_setup_teardown, caplog):
workspace_name = "test"
with workspace_setup_teardown(
workspace_name, delete=False
) as tf, caplog.at_level(logging.INFO):
tf.set_workspace("default")
ret, out, err = tf.delete_workspace(
workspace_name, current_path, force=IsFlagged,
)
assert ret == 0
assert err == ""
assert (
f"Command: terraform workspace delete -force test {current_path}"
in caplog.messages
)
"""
def test_list_workspace(self):
tf = Terraform(working_dir=current_path) tf = Terraform(working_dir=current_path)
tf.import_cmd('aws_instance.foo', 'i-abc1234', no_color=IsFlagged) workspaces = tf.list_workspace()
assert 'command: terraform import -no-color aws_instance.foo i-abc1234' in string_logger() assert len(workspaces) > 0
assert 'default' in workspaces

View file

@ -0,0 +1,62 @@
{
"version": 3,
"terraform_version": "0.7.10",
"serial": 0,
"lineage": "d03ecdf7-8be0-4593-a952-1d8127875119",
"modules": [
{
"path": [
"default"
],
"outputs": {},
"resources": {
"aws_instance.ubuntu-1404": {
"type": "aws_instance",
"depends_on": [],
"primary": {
"id": "i-84d10edb",
"attributes": {
"ami": "ami-9abea4fb",
"associate_public_ip_address": "true",
"availability_zone": "us-west-2b",
"disable_api_termination": "false",
"ebs_block_device.#": "0",
"ebs_optimized": "false",
"ephemeral_block_device.#": "0",
"iam_instance_profile": "",
"id": "i-84d10edb",
"instance_state": "running",
"instance_type": "t2.micro",
"key_name": "",
"monitoring": "false",
"network_interface_id": "eni-46544f07",
"private_dns": "ip-172-31-25-244.us-west-2.compute.internal",
"private_ip": "172.31.25.244",
"public_dns": "ec2-35-162-30-219.us-west-2.compute.amazonaws.com",
"public_ip": "35.162.30.219",
"root_block_device.#": "1",
"root_block_device.0.delete_on_termination": "true",
"root_block_device.0.iops": "100",
"root_block_device.0.volume_size": "8",
"root_block_device.0.volume_type": "gp2",
"security_groups.#": "0",
"source_dest_check": "true",
"subnet_id": "subnet-d2c0f0a6",
"tags.%": "0",
"tenancy": "default",
"vpc_security_group_ids.#": "1",
"vpc_security_group_ids.619359045": "sg-9fc7dcfd"
},
"meta": {
"schema_version": "1"
},
"tainted": false
},
"deposed": [],
"provider": ""
}
},
"depends_on": []
}
]
}

View file

@ -0,0 +1,62 @@
{
"version": 3,
"terraform_version": "0.7.10",
"serial": 0,
"lineage": "d03ecdf7-8be0-4593-a952-1d8127875119",
"modules": [
{
"path": [
"default_backend"
],
"outputs": {},
"resources": {
"aws_instance.ubuntu-1404": {
"type": "aws_instance",
"depends_on": [],
"primary": {
"id": "i-84d10edb",
"attributes": {
"ami": "ami-9abea4fb",
"associate_public_ip_address": "true",
"availability_zone": "us-west-2b",
"disable_api_termination": "false",
"ebs_block_device.#": "0",
"ebs_optimized": "false",
"ephemeral_block_device.#": "0",
"iam_instance_profile": "",
"id": "i-84d10edb",
"instance_state": "running",
"instance_type": "t2.micro",
"key_name": "",
"monitoring": "false",
"network_interface_id": "eni-46544f07",
"private_dns": "ip-172-31-25-244.us-west-2.compute.internal",
"private_ip": "172.31.25.244",
"public_dns": "ec2-35-162-30-219.us-west-2.compute.amazonaws.com",
"public_ip": "35.162.30.219",
"root_block_device.#": "1",
"root_block_device.0.delete_on_termination": "true",
"root_block_device.0.iops": "100",
"root_block_device.0.volume_size": "8",
"root_block_device.0.volume_type": "gp2",
"security_groups.#": "0",
"source_dest_check": "true",
"subnet_id": "subnet-d2c0f0a6",
"tags.%": "0",
"tenancy": "default",
"vpc_security_group_ids.#": "1",
"vpc_security_group_ids.619359045": "sg-9fc7dcfd"
},
"meta": {
"schema_version": "1"
},
"tainted": false
},
"deposed": [],
"provider": ""
}
},
"depends_on": []
}
]
}

View file

@ -0,0 +1 @@
test_var = "True"

View file

@ -5,12 +5,12 @@ variable "test_var" {
provider "archive" {} provider "archive" {}
variable "test_list_var" { variable "test_list_var" {
type = "list" type = list(string)
default = ["a", "b"] default = ["a", "b"]
} }
variable "test_map_var" { variable "test_map_var" {
type = "map" type = map
default = { default = {
"a" = "a" "a" = "a"
@ -19,13 +19,13 @@ variable "test_map_var" {
} }
output "test_output" { output "test_output" {
value = "${var.test_var}" value = var.test_var
} }
output "test_list_output" { output "test_list_output" {
value = "${var.test_list_var}" value = var.test_list_var
} }
output "test_map_output" { output "test_map_output" {
value = "${var.test_map_var}" value = var.test_map_var
} }

View file

@ -1,16 +1,16 @@
variable "ami" { variable "ami" {
default = "foo" default = "foo"
type = "string" type = string
} }
variable "list" { variable "list" {
default = [] default = []
type = "list" type = list
} }
variable "map" { variable "map" {
default = {} default = {}
type = "map" type = map
} }
resource "aws_instance" "bar" { resource "aws_instance" "bar" {

12
tox.ini
View file

@ -1,12 +0,0 @@
# content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py27, py35, py36
[testenv]
deps=pytest
commands=py.test test
[travis]
python =
2.7: py27
3.5: py35
3.6: py36