Compare commits
163 commits
Author | SHA1 | Date | |
---|---|---|---|
7c4cd23e56 | |||
3fd4a72d3d | |||
0fb8218dbf | |||
3c92a8b2e6 | |||
be63bfde3e | |||
a3d97a0d04 | |||
c1c25a016f | |||
0abb2370e4 | |||
5af484bcce | |||
722ba5f716 | |||
|
d88670503b | ||
bc27522fb5 | |||
441bc497d1 | |||
ddb8e4160d | |||
71c544bad8 | |||
1a7955b738 | |||
9c9d7dc14b | |||
f94f3a7c43 | |||
026e5f260a | |||
35b52fdbe8 | |||
d44570e9bb | |||
5e204617d0 | |||
7d50eec689 | |||
b509685c62 | |||
30daf13244 | |||
fd431b229d | |||
58e4e9f065 | |||
124300c271 | |||
382c571d42 | |||
cb30395d4d | |||
dcbeb0e934 | |||
9dd3664d8c | |||
ae6bd30923 | |||
ffbd97fb8f | |||
d5ad3c7628 | |||
4ba2857db7 | |||
262685efda | |||
9863c3c115 | |||
5bdfb662bc | |||
73b8fb0707 | |||
09139ba1b1 | |||
e92972db08 | |||
7821fb431a | |||
2d57a67c63 | |||
816b76070e | |||
4483f54987 | |||
42b4f18b3f | |||
fd959877a7 | |||
f8f3e41529 | |||
e3e5ad775f | |||
021db11acb | |||
46b20fe086 | |||
3d2c7ae396 | |||
412ea860f8 | |||
085273a726 | |||
f913b1d6b7 | |||
ee097cb230 | |||
5d1ee33790 | |||
607cdaf408 | |||
9c73aaec5e | |||
fbc603d1f9 | |||
de7feeee19 | |||
6048bf41d5 | |||
e84ebc2bfb | |||
af20c6da16 | |||
f2bb91560a | |||
9389fd5a24 | |||
437f372fa3 | |||
d8351ba873 | |||
b3dfcb70e0 | |||
f46b890778 | |||
4ab98db4aa | |||
d6a3e3e722 | |||
ed882822e7 | |||
4c17cb661b | |||
adb1fa06a4 | |||
06663390c2 | |||
40a7cba90d | |||
|
b80c977c79 | ||
|
80ca26b26a | ||
|
f6003d8e2f | ||
|
e1ae747ebf | ||
|
edfc33b0bd | ||
|
7080f38a93 | ||
|
785e9a3ce7 | ||
|
44675d38f5 | ||
|
bcd523ff67 | ||
|
b373f85311 | ||
|
053f956774 | ||
|
d83132b61a | ||
|
09120a943b | ||
|
ba72a09409 | ||
|
134786ebf0 | ||
|
6ea1320bef | ||
|
151c5dc92a | ||
|
61a76803d4 | ||
|
ffd9e1de2a | ||
|
47cec4d211 | ||
|
fc584c2a48 | ||
|
a33aebce1c | ||
|
632b653b2b | ||
|
bda8b97e59 | ||
|
6da2d853fc | ||
|
3efa8b2f65 | ||
|
e915a81f7c | ||
|
7c4ac21fb3 | ||
|
89914bbbd8 | ||
|
bcc1321563 | ||
|
3eaaec37d0 | ||
|
09fdcf2f11 | ||
|
c0ff5bf65a | ||
|
4a76a44c91 | ||
|
f39b7e237e | ||
|
a63e9dad24 | ||
|
76351ba371 | ||
|
ce788acc3f | ||
|
6d8dde7991 | ||
|
0a7162a5ce | ||
|
172ab6b509 | ||
|
7056a853da | ||
|
c79e807028 | ||
|
3a32241cf1 | ||
|
a05fc98e8d | ||
|
c6d37cbaeb | ||
|
35db7917b0 | ||
|
29daa9c40a | ||
|
c46560eeed | ||
|
c000d00504 | ||
|
70920d828c | ||
|
01e3dbb05a | ||
|
3ddab331f0 | ||
|
64e804e7a0 | ||
|
0228de1a60 | ||
|
749bec79e4 | ||
|
57a80d7e71 | ||
|
05c44c2cdc | ||
|
832fc9eb7d | ||
|
576197d768 | ||
|
813d23d759 | ||
|
99950cb03c | ||
|
a27362a145 | ||
|
e2b7bd4686 | ||
|
fe0e651607 | ||
|
e9255b118c | ||
|
bd6528e68e | ||
|
98d221c779 | ||
|
99c67e5fe5 | ||
|
4945f4591d | ||
|
723ab0b79e | ||
|
ec826887f5 | ||
|
9afa4e1d4e | ||
|
a4b36e1418 | ||
|
3c9ea8b526 | ||
|
45860aa39c | ||
|
042868bada | ||
|
68954d1447 | ||
|
b098b5a1d8 | ||
|
4191b40cdc | ||
|
86182a604f | ||
|
3b181bc403 | ||
|
825fa0e54f | ||
|
da5e648e3f | ||
|
0b2eb3b1be |
29 changed files with 1528 additions and 648 deletions
|
@ -1,7 +0,0 @@
|
|||
[bumpversion]
|
||||
current_version = 0.9.0
|
||||
commit = True
|
||||
tag = False
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -1,10 +1,39 @@
|
|||
# terraform
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
|
||||
# python
|
||||
*.pyc
|
||||
*.egg-info
|
||||
.idea
|
||||
.cache
|
||||
__pycache__
|
||||
/.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
|
||||
Icon
|
||||
.DS_Store
|
||||
/.tox/
|
||||
env/
|
||||
Icon
|
||||
.clj-kondo
|
117
.gitlab-ci.yml
Normal file
117
.gitlab-ci.yml
Normal 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
27
.pre-commit-config.yaml
Normal 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
|
52
.travis.yml
52
.travis.yml
|
@ -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
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -1,6 +1,24 @@
|
|||
# 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]
|
||||
### Fixed
|
||||
1. [#12] Output function doesn't accept parameter 'module'
|
||||
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
0
CONTRIBUTING.md
Normal file
|
@ -1,3 +1,3 @@
|
|||
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
|
||||
|
|
|
@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
96
README.md
96
README.md
|
@ -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
|
||||
|
||||
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/
|
||||
|
||||
### Status
|
||||
[![Build Status](https://travis-ci.org/beelit94/python-terraform.svg?branch=develop)](https://travis-ci.org/beelit94/python-terraform)
|
||||
|
||||
## Installation
|
||||
pip install python-terraform
|
||||
|
||||
|
||||
## Usage
|
||||
#### For any terraform command
|
||||
|
||||
from python_terraform import *
|
||||
from dda_python_terraform import *
|
||||
t = Terraform()
|
||||
return_code, stdout, stderr = t.<cmd_name>(*arguments, **options)
|
||||
|
||||
|
@ -20,53 +23,53 @@ 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,
|
||||
`import` here could be called by
|
||||
|
||||
from python_terraform import *
|
||||
from dda_python_terraform import *
|
||||
t = Terraform()
|
||||
return_code, stdout, stderr = t.import_cmd(*arguments, **options)
|
||||
|
||||
or just call cmd method directly
|
||||
|
||||
from python_terraform import *
|
||||
from dda_python_terraform import *
|
||||
t = Terraform()
|
||||
return_code, stdout, stderr = t.cmd(<cmd_name>, *arguments, **options)
|
||||
|
||||
|
||||
#### For any argument
|
||||
simply pass the string to arguments of the method, for example,
|
||||
|
||||
terraform apply target_dir
|
||||
terraform apply target_dir
|
||||
--> <instance>.apply('target_dir')
|
||||
terraform import aws_instance.foo i-abcd1234
|
||||
terraform import aws_instance.foo i-abcd1234
|
||||
--> <instance>.import('aws_instance.foo', 'i-abcd1234')
|
||||
|
||||
#### For any options
|
||||
|
||||
|
||||
* dash to underscore
|
||||
|
||||
remove first dash, and then use underscore to replace dash symbol as option name
|
||||
|
||||
|
||||
ex. -no-color --> no_color
|
||||
|
||||
* for a simple flag option
|
||||
|
||||
use ```IsFlagged/None``` as value for raising/not raising flag, for example,
|
||||
|
||||
terraform taint -allow-missing
|
||||
use ```IsFlagged/None``` as value for raising/not raising flag, for example,
|
||||
|
||||
terraform taint -allow-missing
|
||||
--> <instance>.taint(allow_missing=IsFlagged)
|
||||
terraform taint
|
||||
terraform taint
|
||||
--> <instance>.taint(allow_missing=None) or <instance>.taint()
|
||||
terraform apply -no-color
|
||||
--> <instance>.apply(no_color=IsFlagged)
|
||||
|
||||
|
||||
* for a boolean value option
|
||||
|
||||
|
||||
assign True or False, for example,
|
||||
|
||||
|
||||
terraform apply -refresh=true --> <instance>.apply(refresh=True)
|
||||
|
||||
|
||||
* if a flag could be used multiple times, assign a list to it's value
|
||||
|
||||
|
||||
terraform apply -target=aws_instance.foo[1] -target=aws_instance.foo[2]
|
||||
--->
|
||||
--->
|
||||
<instance>.apply(target=['aws_instance.foo[1]', 'aws_instance.foo[2]'])
|
||||
* for the "var" flag, assign dictionary to it
|
||||
|
||||
|
@ -79,69 +82,70 @@ 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 python_terraform import Terraform
|
||||
from dda_python_terraform import Terraform
|
||||
t = Terraform()
|
||||
return_code, stdout, stderr = t.<cmd_name>(capture_output=False)
|
||||
|
||||
## Examples
|
||||
### Have a test.tf file under folder "/home/test"
|
||||
### Have a test.tf file under folder "/home/test"
|
||||
#### 1. apply with variables a=b, c=d, refresh=false, no color in the output
|
||||
In shell:
|
||||
|
||||
cd /home/test
|
||||
terraform apply -var='a=b' -var='c=d' -refresh=false -no-color
|
||||
|
||||
|
||||
In python-terraform:
|
||||
|
||||
from python_terraform import *
|
||||
from dda_python_terraform import *
|
||||
tf = Terraform(working_dir='/home/test')
|
||||
tf.apply(no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'})
|
||||
|
||||
|
||||
or
|
||||
|
||||
from python_terraform import *
|
||||
from dda_python_terraform import *
|
||||
tf = Terraform()
|
||||
tf.apply('/home/test', no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'})
|
||||
|
||||
or
|
||||
|
||||
from python_terraform import *
|
||||
from dda_python_terraform import *
|
||||
tf = Terraform(working_dir='/home/test', variables={'a':'b', 'c':'d'})
|
||||
tf.apply(no_color=IsFlagged, refresh=False)
|
||||
|
||||
|
||||
#### 2. fmt command, diff=true
|
||||
In shell:
|
||||
|
||||
cd /home/test
|
||||
terraform fmt -diff=true
|
||||
|
||||
terraform fmt -diff=true
|
||||
|
||||
In python-terraform:
|
||||
|
||||
from python_terraform import *
|
||||
|
||||
from dda_python_terraform import *
|
||||
tf = terraform(working_dir='/home/test')
|
||||
tf.fmt(diff=True)
|
||||
|
||||
|
||||
|
||||
## default values
|
||||
for apply/plan/destroy command, assign with following default value to make
|
||||
for apply/plan/destroy command, assign with following default value to make
|
||||
caller easier in python
|
||||
|
||||
1. ```input=False```, in this case process won't hang because you missing a variable
|
||||
1. ```no_color=IsFlagged```, in this case, stdout of result is easier for parsing
|
||||
|
||||
## Implementation
|
||||
IMHO, how terraform design boolean options is confusing.
|
||||
IMHO, how terraform design boolean options is confusing.
|
||||
Take `input=True` and `-no-color` option of `apply` command for example,
|
||||
they're all boolean value but with different option type.
|
||||
This make api caller don't have a general rule to follow but to do
|
||||
they're all boolean value but with different option type.
|
||||
This make api caller don't have a general rule to follow but to do
|
||||
a exhaustive method implementation which I don't prefer to.
|
||||
Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option
|
||||
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
|
||||
|
|
9
dda_python_terraform/__init__.py
Normal file
9
dda_python_terraform/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
"""Module providing wrapper for terraform."""
|
||||
from .terraform import (
|
||||
IsFlagged,
|
||||
IsNotFlagged,
|
||||
Terraform,
|
||||
TerraformCommandError,
|
||||
VariableFiles,
|
||||
)
|
||||
from .tfstate import Tfstate
|
578
dda_python_terraform/terraform.py
Normal file
578
dda_python_terraform/terraform.py
Normal 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 = []
|
35
dda_python_terraform/tfstate.py
Normal file
35
dda_python_terraform/tfstate.py
Normal 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
12
doc/releasing.md
Normal 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
|
||||
```
|
|
@ -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 = []
|
|
@ -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()
|
|
@ -1,3 +1 @@
|
|||
tox-pyenv
|
||||
pytest
|
||||
tox
|
||||
packaging
|
||||
|
|
11
requirements_dev.txt
Normal file
11
requirements_dev.txt
Normal 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
|
|
@ -1,2 +1,10 @@
|
|||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[isort]
|
||||
line_length=88
|
||||
known_third_party=
|
||||
indent=' '
|
||||
multi_line_output=3
|
||||
sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||
include_trailing_comma=true
|
||||
|
|
48
setup.py
48
setup.py
|
@ -7,12 +7,13 @@ except ImportError:
|
|||
from distutils.core import setup
|
||||
|
||||
dependencies = []
|
||||
module_name = 'python-terraform'
|
||||
short_description = 'This is a python module provide a wrapper ' \
|
||||
'of terraform command line tool'
|
||||
module_name = "dda-python-terraform"
|
||||
short_description = (
|
||||
"This is a python module provide a wrapper " "of terraform command line tool"
|
||||
)
|
||||
|
||||
try:
|
||||
with open('DESCRIPTION.rst') as f:
|
||||
with open("DESCRIPTION.rst") as f:
|
||||
long_description = f.read()
|
||||
except IOError:
|
||||
long_description = short_description
|
||||
|
@ -20,36 +21,37 @@ except IOError:
|
|||
|
||||
setup(
|
||||
name=module_name,
|
||||
version='0.9.0',
|
||||
url='https://github.com/beelit94/python-terraform',
|
||||
license='MIT',
|
||||
author='Freddy Tan',
|
||||
author_email='beelit94@gmail.com',
|
||||
version="2.1.2-dev",
|
||||
url="https://repo.prod.meissa.de/meissa/dda-python-terraform",
|
||||
license="MIT",
|
||||
author="Freddy Tan, meissa team",
|
||||
author_email="buero@meissa.de",
|
||||
description=short_description,
|
||||
long_description=long_description,
|
||||
packages=['python_terraform'],
|
||||
packages=["dda_python_terraform"],
|
||||
package_data={},
|
||||
platforms='any',
|
||||
platforms="any",
|
||||
install_requires=dependencies,
|
||||
tests_require=["pytest"],
|
||||
python_requires=">=3.6",
|
||||
classifiers=[
|
||||
# As from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
# 'Development Status :: 1 - Planning',
|
||||
# 'Development Status :: 2 - Pre-Alpha',
|
||||
# 'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
"Development Status :: 4 - Beta",
|
||||
# 'Development Status :: 5 - Production/Stable',
|
||||
# 'Development Status :: 6 - Mature',
|
||||
# 'Development Status :: 7 - Inactive',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: MacOS',
|
||||
'Operating System :: Unix',
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Unix",
|
||||
# 'Operating System :: Windows',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
]
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -5,12 +5,12 @@ variable "test_var" {
|
|||
provider "archive" {}
|
||||
|
||||
variable "test_list_var" {
|
||||
type = "list"
|
||||
type = list(string)
|
||||
default = ["a", "b"]
|
||||
}
|
||||
|
||||
variable "test_map_var" {
|
||||
type = "map"
|
||||
type = map
|
||||
|
||||
default = {
|
||||
"a" = "a"
|
||||
|
@ -19,13 +19,13 @@ variable "test_map_var" {
|
|||
}
|
||||
|
||||
output "test_output" {
|
||||
value = "${var.test_var}"
|
||||
value = var.test_var
|
||||
}
|
||||
|
||||
output "test_list_output" {
|
||||
value = "${var.test_list_var}"
|
||||
value = var.test_list_var
|
||||
}
|
||||
|
||||
output "test_map_output" {
|
||||
value = "${var.test_map_var}"
|
||||
value = var.test_map_var
|
||||
}
|
||||
|
|
|
@ -1,70 +1,209 @@
|
|||
try:
|
||||
from cStringIO import StringIO # Python 2
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
from python_terraform import *
|
||||
import pytest
|
||||
import os
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
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)
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
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!"
|
||||
STRING_CASES = [
|
||||
[
|
||||
lambda x: x.generate_cmd_string('apply', 'the_folder',
|
||||
no_color=IsFlagged),
|
||||
"terraform apply -no-color the_folder"
|
||||
],
|
||||
[
|
||||
lambda x: x.generate_cmd_string('push', 'path', vcs=True,
|
||||
token='token',
|
||||
atlas_address='url'),
|
||||
"terraform push -vcs=true -token=token -atlas-address=url path"
|
||||
],
|
||||
]
|
||||
[
|
||||
lambda x: x.generate_cmd_string(
|
||||
{}, "apply", "the_folder", no_color=IsFlagged),
|
||||
"terraform apply -no-color the_folder",
|
||||
],
|
||||
[
|
||||
lambda x: x.generate_cmd_string({},
|
||||
"push", "path", vcs=True, token="token", atlas_address="url"
|
||||
),
|
||||
"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 = [
|
||||
['method', 'expected_output', 'expected_ret_code', 'expected_logs', 'folder'],
|
||||
CMD_CASES_0_x = [
|
||||
[
|
||||
"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'}) ,
|
||||
"doesn't need to do anything",
|
||||
lambda x: x.cmd(
|
||||
{},
|
||||
"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,
|
||||
'',
|
||||
'var_to_output'
|
||||
False,
|
||||
"",
|
||||
"var_to_output",
|
||||
],
|
||||
# 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,
|
||||
'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
|
||||
[
|
||||
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,
|
||||
'',
|
||||
'var_to_output'
|
||||
]
|
||||
]
|
||||
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",
|
||||
"",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
@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):
|
||||
target = os.path.join(current_path, 'bad_fmt', 'test.backup')
|
||||
orgin = os.path.join(current_path, 'bad_fmt', 'test.tf')
|
||||
shutil.copy(orgin,
|
||||
target)
|
||||
target = os.path.join(current_path, "bad_fmt", "test.backup")
|
||||
orgin = os.path.join(current_path, "bad_fmt", "test.tf")
|
||||
shutil.copy(orgin, target)
|
||||
|
||||
def td():
|
||||
shutil.move(target, orgin)
|
||||
|
@ -73,28 +212,51 @@ def fmt_test_file(request):
|
|||
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()
|
||||
def string_logger(request):
|
||||
log_stream = StringIO()
|
||||
handler = logging.StreamHandler(log_stream)
|
||||
root_logger.addHandler(handler)
|
||||
def workspace_setup_teardown():
|
||||
"""Fixture used in workspace related tests.
|
||||
|
||||
def td():
|
||||
root_logger.removeHandler(handler)
|
||||
log_stream.close()
|
||||
Create and tear down a workspace
|
||||
*Use as a contextmanager*
|
||||
"""
|
||||
|
||||
request.addfinalizer(td)
|
||||
return lambda: str(log_stream.getvalue())
|
||||
@contextmanager
|
||||
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):
|
||||
def teardown_method(self, method):
|
||||
""" teardown any state that was previously setup with a setup_method
|
||||
call.
|
||||
"""
|
||||
class TestTerraform:
|
||||
def teardown_method(self, _) -> None:
|
||||
"""Teardown any state that was previously setup with a setup_method 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):
|
||||
dirnames[:] = [d for d in dirnames if d not in exclude]
|
||||
for filename in fnmatch.filter(filenames, pattern):
|
||||
f = os.path.join(root, filename)
|
||||
os.remove(f)
|
||||
|
@ -102,134 +264,294 @@ class TestTerraform(object):
|
|||
d = os.path.join(root, dirname)
|
||||
shutil.rmtree(d)
|
||||
|
||||
purge('.', '*.tfstate')
|
||||
purge('.', '*.terraform')
|
||||
purge('.', FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS)
|
||||
purge(".", "*.tfstate")
|
||||
purge(".", "*.tfstate.backup")
|
||||
purge(".", "*.terraform")
|
||||
purge(".", FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS)
|
||||
|
||||
@pytest.mark.parametrize([
|
||||
"method", "expected"
|
||||
], STRING_CASES)
|
||||
def test_generate_cmd_string(self, method, expected):
|
||||
tf = Terraform(working_dir=current_path)
|
||||
@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)
|
||||
result = method(tf)
|
||||
|
||||
strs = expected.split()
|
||||
for s in strs:
|
||||
assert s in result
|
||||
|
||||
@pytest.mark.parametrize(*CMD_CASES)
|
||||
def test_cmd(self, method, expected_output, expected_ret_code, expected_logs, string_logger, folder):
|
||||
tf = Terraform(working_dir=current_path)
|
||||
tf.init(folder)
|
||||
ret, out, err = method(tf)
|
||||
logs = string_logger()
|
||||
logs = logs.replace('\n', '')
|
||||
@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: Callable[..., str],
|
||||
expected_output: str,
|
||||
expected_ret_code: int,
|
||||
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_ret_code == ret
|
||||
assert expected_logs in logs
|
||||
assert expected_logs in caplog.text
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("folder", "variables", "var_files", "expected_output", "options"),
|
||||
[
|
||||
("var_to_output",
|
||||
{'test_var': 'test'}, None, "test_output=test", {}),
|
||||
("var_to_output", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]", {}),
|
||||
("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, None, "test_map_output={a=ab=bc=cd=d}", {}),
|
||||
("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, 'var_to_output/test_map_var.json', "test_map_output={a=ab=bc=cd=de=ef=f}", {}),
|
||||
("var_to_output", {}, None, "\x1b[0m\x1b[1m\x1b[32mApplycomplete!", {"no_color": IsNotFlagged})
|
||||
])
|
||||
|
||||
@pytest.mark.parametrize(*(APPLY_CASES_1_x if version.parse(semantic_version) >= version.parse("1.0.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)
|
||||
# after 0.10.0 we always need to init
|
||||
tf = Terraform(
|
||||
working_dir=current_path, variables=variables, var_file=var_files, terraform_semantic_version=semantic_version
|
||||
)
|
||||
tf.init(folder)
|
||||
ret, out, err = tf.apply(folder, **options)
|
||||
assert ret == 0
|
||||
assert expected_output in out.replace('\n', '').replace(' ', '')
|
||||
assert err == ''
|
||||
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)
|
||||
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(
|
||||
['cmd', 'args', 'options'],
|
||||
["cmd", "args", "options"],
|
||||
[
|
||||
# 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):
|
||||
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)
|
||||
assert ret == 0
|
||||
assert out == ''
|
||||
assert out == ""
|
||||
|
||||
def test_state_data(self):
|
||||
cwd = os.path.join(current_path, 'test_tfstate_file')
|
||||
tf = Terraform(working_dir=cwd, state='tfstate.test')
|
||||
cwd = os.path.join(current_path, "test_tfstate_file")
|
||||
tf = Terraform(working_dir=cwd, state="tfstate.test",
|
||||
terraform_semantic_version=semantic_version)
|
||||
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):
|
||||
cwd = os.path.join(current_path, 'test_tfstate_file')
|
||||
tf = Terraform(working_dir=cwd, state='tfstate.test')
|
||||
assert tf.tfstate.modules[0]['path'] == ['root']
|
||||
cwd = os.path.join(current_path, "test_tfstate_file")
|
||||
tf = Terraform(working_dir=cwd, state="tfstate.test",
|
||||
terraform_semantic_version=semantic_version)
|
||||
assert tf.tfstate.modules[0]["path"] == ["root"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("folder", 'variables'),
|
||||
[
|
||||
("var_to_output", {'test_var': 'test'})
|
||||
]
|
||||
("folder", "variables"), [("var_to_output", {"test_var": "test"})]
|
||||
)
|
||||
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)
|
||||
ret, out, err = tf.apply(folder, var={'test_var': 'test2'},
|
||||
no_color=IsNotFlagged)
|
||||
out = out.replace('\n', '')
|
||||
assert '\x1b[0m\x1b[1m\x1b[32mApply' in out
|
||||
out = tf.output('test_output')
|
||||
assert 'test2' in out
|
||||
ret, out, err = tf.apply(
|
||||
folder, var={"test_var": "test2"}, no_color=IsNotFlagged,
|
||||
)
|
||||
out = out.replace("\n", "")
|
||||
assert "\x1b[0m\x1b[1m\x1b[32mApply" in out
|
||||
out = tf.output(folder, "test_output")
|
||||
assert "test2" in out
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("param"),
|
||||
[
|
||||
({}),
|
||||
({'module': 'test2'}),
|
||||
]
|
||||
)
|
||||
def test_output(self, param, string_logger):
|
||||
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
|
||||
tf.init('var_to_output')
|
||||
tf.apply('var_to_output')
|
||||
result = tf.output('test_output', **param)
|
||||
regex = re.compile("terraform output (-module=test2 -json|-json -module=test2) test_output")
|
||||
log_str = string_logger()
|
||||
if param:
|
||||
assert re.search(regex, log_str), log_str
|
||||
@pytest.mark.parametrize("output_all", [True, False])
|
||||
def test_output(self, caplog: LogCaptureFixture, output_all: bool):
|
||||
expected_value = "test"
|
||||
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
|
||||
)
|
||||
tf.init("var_to_output")
|
||||
tf.apply("var_to_output")
|
||||
params = tuple() if output_all else (required_output,)
|
||||
result = tf.output("var_to_output", *params)
|
||||
if output_all:
|
||||
assert result[required_output]["value"] == expected_value
|
||||
else:
|
||||
assert result == 'test'
|
||||
assert result == expected_value
|
||||
assert expected_value in caplog.messages[-1]
|
||||
|
||||
def test_destroy(self):
|
||||
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
|
||||
tf.init('var_to_output')
|
||||
ret, out, err = tf.destroy('var_to_output')
|
||||
tf = Terraform(working_dir=current_path, variables={
|
||||
"test_var": "test"}, terraform_semantic_version=semantic_version)
|
||||
tf.init("var_to_output")
|
||||
ret, out, err = tf.destroy("var_to_output")
|
||||
assert ret == 0
|
||||
assert 'Destroy complete! Resources: 0 destroyed.' in out
|
||||
assert "Destroy complete! Resources: 0 destroyed." in out
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("plan", "variables", "expected_ret"),
|
||||
[
|
||||
('vars_require_input', {}, 1)
|
||||
]
|
||||
("plan", "variables", "expected_ret"), [("vars_require_input", {}, 1)]
|
||||
)
|
||||
def test_plan(self, plan, variables, expected_ret):
|
||||
tf = Terraform(working_dir=current_path, variables=variables)
|
||||
ret, out, err = tf.plan(plan)
|
||||
assert ret == expected_ret
|
||||
tf = Terraform(working_dir=current_path,
|
||||
variables=variables, terraform_semantic_version=semantic_version)
|
||||
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):
|
||||
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)
|
||||
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.import_cmd('aws_instance.foo', 'i-abc1234', no_color=IsFlagged)
|
||||
assert 'command: terraform import -no-color aws_instance.foo i-abc1234' in string_logger()
|
||||
workspaces = tf.list_workspace()
|
||||
assert len(workspaces) > 0
|
||||
assert 'default' in workspaces
|
||||
|
|
62
test/test_tfstate_file2/terraform.tfstate
Normal file
62
test/test_tfstate_file2/terraform.tfstate
Normal 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": []
|
||||
}
|
||||
]
|
||||
}
|
62
test/test_tfstate_file3/.terraform/terraform.tfstate
Normal file
62
test/test_tfstate_file3/.terraform/terraform.tfstate
Normal 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": []
|
||||
}
|
||||
]
|
||||
}
|
1
test/tfvar_files/test.tfvars
Normal file
1
test/tfvar_files/test.tfvars
Normal file
|
@ -0,0 +1 @@
|
|||
test_var = "True"
|
|
@ -5,12 +5,12 @@ variable "test_var" {
|
|||
provider "archive" {}
|
||||
|
||||
variable "test_list_var" {
|
||||
type = "list"
|
||||
type = list(string)
|
||||
default = ["a", "b"]
|
||||
}
|
||||
|
||||
variable "test_map_var" {
|
||||
type = "map"
|
||||
type = map
|
||||
|
||||
default = {
|
||||
"a" = "a"
|
||||
|
@ -19,13 +19,13 @@ variable "test_map_var" {
|
|||
}
|
||||
|
||||
output "test_output" {
|
||||
value = "${var.test_var}"
|
||||
value = var.test_var
|
||||
}
|
||||
|
||||
output "test_list_output" {
|
||||
value = "${var.test_list_var}"
|
||||
value = var.test_list_var
|
||||
}
|
||||
|
||||
output "test_map_output" {
|
||||
value = "${var.test_map_var}"
|
||||
value = var.test_map_var
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
"e": "e",
|
||||
"f": "f"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
variable "ami" {
|
||||
default = "foo"
|
||||
type = "string"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "list" {
|
||||
default = []
|
||||
type = "list"
|
||||
type = list
|
||||
}
|
||||
|
||||
variable "map" {
|
||||
default = {}
|
||||
type = "map"
|
||||
type = map
|
||||
}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
|
|
12
tox.ini
12
tox.ini
|
@ -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
|
Loading…
Reference in a new issue