├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE.txt ├── Makefile ├── README.md ├── build.py ├── libterraform.go ├── libterraform ├── __init__.py ├── cli.py ├── common.py ├── config.py └── exceptions.py ├── plugin_patch.go ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── cli ├── __init__.py ├── conftest.py ├── test_apply.py ├── test_destroy.py ├── test_fmt.py ├── test_force_unlock.py ├── test_get.py ├── test_graph.py ├── test_import.py ├── test_init.py ├── test_output.py ├── test_plan.py ├── test_providers.py ├── test_refresh.py ├── test_run.py ├── test_show.py ├── test_state.py ├── test_taint.py ├── test_test.py ├── test_untaint.py ├── test_validate.py ├── test_version.py └── test_workspace.py ├── config ├── __init__.py └── test_load_config_dir.py ├── consts.py └── tf ├── sleep └── main.tf └── sleep2 ├── main.tf └── valid_sleep.tftest.hcl /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ ubuntu-20.04, windows-2019, macos-12 ] 13 | python-version: [ '3.7.9', '3.8.10', '3.9.13', '3.10.11', '3.11.9', '3.12.4' ] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Check out repository code 17 | uses: actions/checkout@v2 18 | - name: Set up GoLang 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: '^1.21.5' 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install poetry 27 | run: | 28 | python -m pip install poetry 29 | - name: Fetch terraform repository 30 | run: | 31 | git submodule init 32 | git submodule update 33 | - name: Build distributions 34 | run: | 35 | poetry build -f wheel -vvv 36 | - name: Upload distribution artifacts 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: libterraform-dist 40 | path: dist 41 | build-macos-arm64: 42 | strategy: 43 | matrix: 44 | os: [ macos-latest ] 45 | python-version: [ '3.8.10', '3.9.13', '3.10.11', '3.11.9', '3.12.4' ] 46 | runs-on: ${{ matrix.os }} 47 | steps: 48 | - name: Check out repository code 49 | uses: actions/checkout@v2 50 | - name: Set up GoLang 51 | uses: actions/setup-go@v4 52 | with: 53 | go-version: '^1.21.5' 54 | - name: Set up Python ${{ matrix.python-version }} 55 | uses: actions/setup-python@v2 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | - name: Install poetry 59 | run: | 60 | python -m pip install poetry 61 | - name: Fetch terraform repository 62 | run: | 63 | git submodule init 64 | git submodule update 65 | - name: Build distributions 66 | run: | 67 | poetry build -f wheel -vvv 68 | - name: Upload distribution artifacts 69 | uses: actions/upload-artifact@v3 70 | with: 71 | name: libterraform-dist 72 | path: dist 73 | publish: 74 | needs: [ build, build-macos-arm64 ] 75 | runs-on: macos-latest 76 | steps: 77 | - name: Checkout repository code 78 | uses: actions/checkout@v2 79 | - name: Set up Python 3.10 80 | uses: actions/setup-python@v2 81 | with: 82 | python-version: '3.10' 83 | - name: Download distribution artifacts 84 | uses: actions/download-artifact@v3 85 | with: 86 | name: libterraform-dist 87 | path: dist 88 | - name: Install poetry 89 | run: | 90 | python -m pip install poetry 91 | - name: Create Release 92 | uses: softprops/action-gh-release@v1 93 | - name: Publish to PyPI 94 | env: 95 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 96 | run: | 97 | poetry config pypi-token.pypi $POETRY_PYPI_TOKEN_PYPI 98 | poetry publish 99 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ ubuntu-20.04, windows-2019, macos-12 ] 10 | python-version: [ '3.7.9', '3.8.10', '3.9.13', '3.10.11', '3.11.9', '3.12.4' ] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - name: Check out repository code 14 | uses: actions/checkout@v4 15 | - name: Set up GoLang 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '^1.21.5' 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install poetry pytest 24 | run: | 25 | python -m pip install poetry pytest 26 | - name: Fetch terraform repository 27 | run: | 28 | git submodule init 29 | git submodule update 30 | - name: Build distributions 31 | run: | 32 | poetry build -f wheel -vvv 33 | - name: Run tests 34 | run: | 35 | pytest 36 | test-macos-arm64: 37 | strategy: 38 | matrix: 39 | os: [ macos-latest ] 40 | python-version: [ '3.8.10', '3.9.13', '3.10.11', '3.11.9', '3.12.4' ] 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - name: Check out repository code 44 | uses: actions/checkout@v2 45 | - name: Set up GoLang 46 | uses: actions/setup-go@v4 47 | with: 48 | go-version: '^1.21.5' 49 | - name: Set up Python ${{ matrix.python-version }} 50 | uses: actions/setup-python@v2 51 | with: 52 | python-version: ${{ matrix.python-version }} 53 | - name: Install poetry pytest 54 | run: | 55 | python -m pip install poetry pytest 56 | - name: Fetch terraform repository 57 | run: | 58 | git submodule init 59 | git submodule update 60 | - name: Build distributions 61 | run: | 62 | poetry build -f wheel -vvv 63 | - name: Run tests 64 | run: | 65 | pytest 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | .DS_Store 4 | bin/ 5 | modules-dev/ 6 | /pkg/ 7 | website/.vagrant 8 | website/.bundle 9 | website/build 10 | website/node_modules 11 | website/vendor 12 | .vagrant/ 13 | vendor/ 14 | *.backup 15 | *.bak 16 | *~ 17 | .*.swp 18 | .idea 19 | *.iml 20 | *.test 21 | *.iml 22 | *.so 23 | *.dll 24 | *.h 25 | t.* 26 | 27 | dist 28 | __pycache__ 29 | .pytest_cache 30 | .python-version 31 | 32 | # Coverage 33 | coverage.txt 34 | 35 | # Terraform 36 | .terraform 37 | .terraform.* 38 | *.tfplan 39 | *.tfstate 40 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "terraform"] 2 | path = terraform 3 | url = https://github.com/hashicorp/terraform 4 | shallow = true 5 | [submodule "go-plugin"] 6 | path = go-plugin 7 | url = https://github.com/hashicorp/go-plugin 8 | shallow = true 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Prodesire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Env 2 | export PYTHONDONTWRITEBYTECODE=1 3 | TEST_PATH=./tests 4 | PY3=python3 5 | 6 | help: 7 | @echo "\033[32minit\033[0m" 8 | @echo " Init environment for libterraform." 9 | @echo "\033[32mtest\033[0m" 10 | @echo " Run pytest. Please run \`make build\` first." 11 | @echo "\033[32mbuild\033[0m" 12 | @echo " Build libterraform." 13 | @echo "\033[32mpublish\033[0m" 14 | @echo " Publish libterraform to PyPI." 15 | @echo "\033[32mclean\033[0m" 16 | @echo " Remove python and build artifacts." 17 | @echo "\033[32mclean-pyc\033[0m" 18 | @echo " Remove python artifacts." 19 | @echo "\033[32mclean-build\033[0m" 20 | @echo " Remove build artifacts." 21 | 22 | init: 23 | $(PY3) -m pip install poetry pytest 24 | 25 | test: clean-pyc 26 | $(PY3) -m pytest --color=yes $(TEST_PATH) 27 | 28 | build: 29 | $(PY3) -m poetry build -f wheel 30 | 31 | build-all: 32 | $(PY3) -m poetry env use python3.7 33 | $(PY3) -m poetry build -f wheel 34 | $(PY3) -m poetry env use python3.8 35 | $(PY3) -m poetry build -f wheel 36 | $(PY3) -m poetry env use python3.9 37 | $(PY3) -m poetry build -f wheel 38 | $(PY3) -m poetry env use python3.10 39 | $(PY3) -m poetry build -f wheel 40 | $(PY3) -m poetry env use python3.11 41 | $(PY3) -m poetry build -f wheel 42 | $(PY3) -m poetry env use python3.12 43 | $(PY3) -m poetry build -f wheel 44 | rename 's/-macosx_\d+_/-macosx_12_/' dist/*-macosx_*.whl 45 | 46 | 47 | publish: 48 | $(PY3) -m poetry publish 49 | 50 | clean: clean-pyc clean-build 51 | 52 | clean-pyc: 53 | find . -name '*.pyc' -exec rm -f {} + 54 | find . -name '*.pyo' -exec rm -f {} + 55 | find . -name '*~' -exec rm -f {} + 56 | find . -name '__pycache__' -exec rm -rf {} + 57 | 58 | clean-build: 59 | rm -rf build dist *.egg-info .eggs 60 | find . -name '*.h' -exec rm -f {} + 61 | 62 | format: 63 | $(PY3) -m poetry run isort libterraform tests 64 | $(PY3) -m poetry run ruff format libterraform tests 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python libterraform 2 | 3 | [![libterraform](https://img.shields.io/pypi/v/libterraform.svg)](https://pypi.python.org/pypi/libterraform) 4 | [![libterraform](https://img.shields.io/pypi/l/libterraform.svg)](https://pypi.python.org/pypi/libterraform) 5 | [![libterraform](https://img.shields.io/pypi/pyversions/libterraform.svg)](https://pypi.python.org/pypi/libterraform) 6 | [![Test](https://github.com/Prodesire/py-libterraform/actions/workflows/test.yml/badge.svg)](https://github.com/Prodesire/py-libterraform/actions/workflows/test.yml) 7 | [![libterraform](https://img.shields.io/pypi/dm/libterraform)](https://pypi.python.org/pypi/libterraform) 8 | 9 | Python binding for [Terraform](https://www.terraform.io/). 10 | 11 | ## Installation 12 | 13 | ```bash 14 | $ pip install libterraform 15 | ``` 16 | 17 | > **NOTE** 18 | > - Please install version **0.3.1** or above, which solves the memory leak problem. 19 | > - This library does **not support** multithreading. 20 | 21 | ## Usage 22 | 23 | ### Terraform CLI 24 | 25 | `TerraformCommand` is used to invoke various Terraform commands. 26 | 27 | Now, support all commands (`plan`, `apply`, `destroy` etc.), and return a `CommandResult` object. The `CommandResult` 28 | object has the following properties: 29 | 30 | - `retcode` indicates the command return code. A value of 0 or 2 is normal, otherwise is abnormal. 31 | - `value` represents command output. If `json=True` is specified when executing the command, the output will be loaded 32 | as json. 33 | - `json` indicates whether to load the output as json. 34 | - `error` indicates command error output. 35 | 36 | To get Terraform verison: 37 | 38 | ```python 39 | >>> from libterraform import TerraformCommand 40 | >>> TerraformCommand().version() 41 | 42 | >>> _.value 43 | {'terraform_version': '1.8.4', 'platform': 'darwin_arm64', 'provider_selections': {}, 'terraform_outdated': True} 44 | >>> TerraformCommand().version(json=False) 45 | 46 | >>> _.value 47 | 'Terraform v1.8.4\non darwin_arm64\n' 48 | ``` 49 | 50 | To `init` and `apply` according to Terraform configuration files in the specified directory: 51 | 52 | ```python 53 | >>> from libterraform import TerraformCommand 54 | >>> cli = TerraformCommand('your_terraform_configuration_directory') 55 | >>> cli.init() 56 | 57 | >>> cli.apply() 58 | 59 | ``` 60 | 61 | Additionally, `run()` can execute arbitrary commands, returning a tuple `(retcode, stdout, stderr)`. 62 | 63 | ```python 64 | >>> TerraformCommand.run('version') 65 | (0, 'Terraform v1.8.4\non darwin_arm64\n', '') 66 | >>> TerraformCommand.run('invalid') 67 | (1, '', 'Terraform has no command named "invalid".\n\nTo see all of Terraform\'s top-level commands, run:\n terraform -help\n\n') 68 | ``` 69 | 70 | ### Terraform Config Parser 71 | 72 | `TerraformConfig` is used to parse Terraform config files. 73 | 74 | For now, only supply `TerraformConfig.load_config_dir` method which reads the .tf and .tf.json files in the given 75 | directory as config files and then combines these files into a single Module. This method returns `(mod, diags)` 76 | which are both dict, corresponding to 77 | the [*Module](https://github.com/hashicorp/terraform/blob/2a5420cb9acf8d5f058ad077dade80214486f1c4/internal/configs/module.go#L14) 78 | and [hcl.Diagnostic](https://github.com/hashicorp/hcl/blob/v2.11.1/diagnostic.go#L26) structures in Terraform 79 | respectively. 80 | 81 | ```python 82 | >>> from libterraform import TerraformConfig 83 | >>> mod, _ = TerraformConfig.load_config_dir('your_terraform_configuration_directory') 84 | >>> mod['ManagedResources'].keys() 85 | dict_keys(['time_sleep.wait1', 'time_sleep.wait2']) 86 | ``` 87 | 88 | ## Version comparison 89 | 90 | | libterraform | Terraform | 91 | |-------------------------------------------------------|-------------------------------------------------------------| 92 | | [0.8.0](https://pypi.org/project/libterraform/0.8.0/) | [1.8.4](https://github.com/hashicorp/terraform/tree/v1.8.4) | 93 | | [0.7.0](https://pypi.org/project/libterraform/0.7.0/) | [1.6.6](https://github.com/hashicorp/terraform/tree/v1.6.6) | 94 | | [0.6.0](https://pypi.org/project/libterraform/0.6.0/) | [1.5.7](https://github.com/hashicorp/terraform/tree/v1.5.7) | 95 | | [0.5.0](https://pypi.org/project/libterraform/0.5.0/) | [1.3.0](https://github.com/hashicorp/terraform/tree/v1.3.0) | 96 | | [0.4.0](https://pypi.org/project/libterraform/0.4.0/) | [1.2.2](https://github.com/hashicorp/terraform/tree/v1.2.2) | 97 | | [0.3.1](https://pypi.org/project/libterraform/0.3.1/) | [1.1.7](https://github.com/hashicorp/terraform/tree/v1.1.7) | 98 | 99 | ## Building & Testing 100 | 101 | If you want to develop this library, should first prepare the following environments: 102 | 103 | - [GoLang](https://go.dev/dl/) (Version 1.21.5+) 104 | - [Python](https://www.python.org/downloads/) (Version 3.7~3.12) 105 | - GCC 106 | 107 | Then, initialize git submodule: 108 | 109 | ```bash 110 | $ git submodule init 111 | $ git submodule update 112 | ``` 113 | 114 | `pip install` necessary tools: 115 | 116 | ```bash 117 | $ pip install poetry pytest 118 | ``` 119 | 120 | Now, we can build and test: 121 | 122 | ```bash 123 | $ poetry build -f wheel 124 | $ pytest 125 | ``` 126 | 127 | ## Why use this library? 128 | 129 | Terraform is a great tool for deploying resources. If you need to call the Terraform command in the Python program for 130 | deployment, a new process needs to be created to execute the Terraform command on the system. A typical example of this 131 | is the [python-terraform](https://github.com/beelit94/python-terraform) library. Doing so has the following problems: 132 | 133 | - Requires Terraform commands on the system. 134 | - The overhead of starting a new process is relatively high. 135 | 136 | This library compiles Terraform as a **dynamic link library** in advance, and then loads it for calling. So there is no 137 | need to install Terraform, nor to start a new process. 138 | 139 | In addition, since the Terraform dynamic link library is loaded, this library can further call Terraform's 140 | **internal capabilities**, such as parsing Terraform config files. 141 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import shutil 4 | import subprocess 5 | 6 | lib_filename = 'libterraform.dll' if platform.system() == 'Windows' else 'libterraform.so' 7 | header_filename = 'libterraform.h' 8 | tf_filename = 'libterraform.go' 9 | root = os.path.dirname(os.path.abspath(__file__)) 10 | terraform_dirname = os.path.join(root, 'terraform') 11 | tf_path = os.path.join(root, tf_filename) 12 | tf_package_name = 'github.com/hashicorp/terraform' 13 | plugin_patch_filename = 'plugin_patch.go' 14 | plugin_dirname = os.path.join(root, 'go-plugin') 15 | plugin_patch_path = os.path.join(root, plugin_patch_filename) 16 | plugin_package_name = 'github.com/hashicorp/go-plugin' 17 | 18 | 19 | class BuildError(Exception): 20 | pass 21 | 22 | 23 | def build(setup_kwargs): 24 | """ 25 | This function is mandatory in order to build the extensions. 26 | """ 27 | if not os.path.exists(os.path.join(terraform_dirname, '.git')): 28 | raise BuildError(f'The directory {terraform_dirname} not exists or init. ' 29 | f'Please execute `git submodule init && git submodule update` to init it.') 30 | if not os.path.exists(os.path.join(plugin_dirname, '.git')): 31 | raise BuildError(f'The directory {plugin_dirname} not exists or init. ' 32 | f'Please execute `git submodule init && git submodule update` to init it.') 33 | 34 | target_plugin_patch_path = os.path.join(plugin_dirname, plugin_patch_filename) 35 | target_tf_path = os.path.join(terraform_dirname, tf_filename) 36 | target_tf_mod_path = os.path.join(terraform_dirname, 'go.mod') 37 | lib_path = os.path.join(terraform_dirname, lib_filename) 38 | header_path = os.path.join(terraform_dirname, header_filename) 39 | 40 | # Patch go-plugin 41 | print(' - Patching go-plugin package') 42 | shutil.copyfile(plugin_patch_path, target_plugin_patch_path) 43 | with open(target_tf_mod_path) as f: 44 | mod_content = f.read() 45 | with open(target_tf_mod_path, 'w') as f: 46 | modified_mod_content = f'{mod_content}\n' \ 47 | f'replace github.com/hashicorp/go-plugin v1.4.3 => ../go-plugin' 48 | f.write(modified_mod_content) 49 | 50 | # Build libterraform 51 | shutil.copyfile(tf_path, target_tf_path) 52 | try: 53 | print(' - Building libterraform') 54 | subprocess.check_call( 55 | ['go', 'build', '-buildmode=c-shared', f'-o={lib_filename}', 56 | "-ldflags", "-X github.com/hashicorp/terraform/version.dev=no", tf_package_name], 57 | cwd=terraform_dirname 58 | ) 59 | shutil.move(lib_path, os.path.join(root, 'libterraform', lib_filename)) 60 | finally: 61 | # Remove external files 62 | for path in (target_plugin_patch_path, target_tf_path, header_path, lib_path): 63 | if os.path.exists(path): 64 | os.remove(path) 65 | # Recover go.mod 66 | with open(target_tf_mod_path, 'w') as f: 67 | f.write(mod_content) 68 | 69 | return setup_kwargs 70 | 71 | 72 | if __name__ == '__main__': 73 | build({}) 74 | -------------------------------------------------------------------------------- /libterraform.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/apparentlymart/go-shquot/shquot" 8 | "github.com/hashicorp/go-plugin" 9 | svchost "github.com/hashicorp/terraform-svchost" 10 | "github.com/hashicorp/terraform-svchost/disco" 11 | "github.com/hashicorp/terraform/internal/addrs" 12 | backendInit "github.com/hashicorp/terraform/internal/backend/init" 13 | "github.com/hashicorp/terraform/internal/command" 14 | "github.com/hashicorp/terraform/internal/command/cliconfig" 15 | "github.com/hashicorp/terraform/internal/command/format" 16 | "github.com/hashicorp/terraform/internal/command/views" 17 | "github.com/hashicorp/terraform/internal/command/webbrowser" 18 | "github.com/hashicorp/terraform/internal/configs" 19 | "github.com/hashicorp/terraform/internal/didyoumean" 20 | "github.com/hashicorp/terraform/internal/experiments" 21 | "github.com/hashicorp/terraform/internal/getproviders" 22 | "github.com/hashicorp/terraform/internal/httpclient" 23 | "github.com/hashicorp/terraform/internal/logging" 24 | "github.com/hashicorp/terraform/internal/terminal" 25 | "github.com/hashicorp/terraform/version" 26 | "github.com/hashicorp/cli" 27 | "github.com/mitchellh/colorstring" 28 | "go.opentelemetry.io/otel/trace" 29 | "log" 30 | "os" 31 | "os/signal" 32 | "path/filepath" 33 | "runtime" 34 | "strings" 35 | "unsafe" 36 | ) 37 | 38 | /* 39 | #include 40 | */ 41 | import "C" 42 | 43 | // ********************************************** 44 | // CLI 45 | // ********************************************** 46 | 47 | var shutdownChs = make(map[chan struct{}]struct{}) 48 | var logFile *os.File 49 | var origStdout = os.Stdout 50 | var origStderr = os.Stderr 51 | 52 | func init() { 53 | signalCh := make(chan os.Signal, 4) 54 | signal.Notify(signalCh, ignoreSignals...) 55 | signal.Notify(signalCh, forwardSignals...) 56 | go func() { 57 | for { 58 | <-signalCh 59 | log.Printf("[INFO] Received signal, shutting down") 60 | for shutdownCh := range shutdownChs { 61 | shutdownCh <- struct{}{} 62 | } 63 | log.Printf("[INFO] Received signal, shut down success") 64 | } 65 | }() 66 | } 67 | 68 | //export RunCli 69 | func RunCli(cArgc C.int, cArgv **C.char, cStdOutFd C.int, cStdErrFd C.int) C.int { 70 | defer logging.PanicHandler() 71 | 72 | var err error 73 | 74 | // Convert C variables to Go variables 75 | os.Args = os.Args[:0] 76 | os.Args = append(os.Args, "Terraform") 77 | argc := int(cArgc) 78 | slice := unsafe.Slice(cArgv, argc) 79 | for _, s := range slice { 80 | arg := C.GoString(s) 81 | os.Args = append(os.Args, arg) 82 | } 83 | 84 | // Override stdout and stdin by given std fd 85 | 86 | Stdout := os.NewFile(uintptr(cStdOutFd), "libterraform/pipe/stdout") 87 | Stderr := os.NewFile(uintptr(cStdErrFd), "libterraform/pipe/stderr") 88 | os.Stdout = Stdout 89 | os.Stderr = Stderr 90 | Ui = &ui{&cli.BasicUi{ 91 | Writer: Stdout, 92 | ErrorWriter: Stderr, 93 | Reader: os.Stdin, 94 | }} 95 | 96 | defer func() { 97 | os.Stdout = origStdout 98 | os.Stderr = origStderr 99 | Stdout.Close() 100 | Stderr.Close() 101 | if len(checkpointResult) > 0 { 102 | <-checkpointResult 103 | } 104 | }() 105 | 106 | err = openTelemetryInit() 107 | if err != nil { 108 | // openTelemetryInit can only fail if Terraform was run with an 109 | // explicit environment variable to enable telemetry collection, 110 | // so in typical use we cannot get here. 111 | Ui.Error(fmt.Sprintf("Could not initialize telemetry: %s", err)) 112 | Ui.Error(fmt.Sprintf("Unset environment variable %s if you don't intend to collect telemetry from Terraform.", openTelemetryExporterEnvVar)) 113 | return 1 114 | } 115 | var ctx context.Context 116 | var otelSpan trace.Span 117 | { 118 | // At minimum we emit a span covering the entire command execution. 119 | _, displayArgs := shquot.POSIXShellSplit(os.Args) 120 | ctx, otelSpan = tracer.Start(context.Background(), fmt.Sprintf("terraform %s", displayArgs)) 121 | defer otelSpan.End() 122 | } 123 | 124 | tmpLogPath := os.Getenv(envTmpLogPath) 125 | if tmpLogPath != "" { 126 | f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666) 127 | if err == nil { 128 | defer f.Close() 129 | 130 | log.Printf("[DEBUG] Adding temp file log sink: %s", f.Name()) 131 | logging.RegisterSink(f) 132 | } else { 133 | log.Printf("[ERROR] Could not open temp log file: %v", err) 134 | } 135 | } 136 | 137 | log.Printf( 138 | "[INFO] Terraform version: %s %s", 139 | Version, VersionPrerelease) 140 | log.Printf("[INFO] Go runtime version: %s", runtime.Version()) 141 | log.Printf("[INFO] CLI args: %#v", os.Args) 142 | 143 | streams, err := terminal.Init() 144 | if err != nil { 145 | Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err)) 146 | return 1 147 | } 148 | if streams.Stdout.IsTerminal() { 149 | log.Printf("[TRACE] Stdout is a terminal of width %d", streams.Stdout.Columns()) 150 | } else { 151 | log.Printf("[TRACE] Stdout is not a terminal") 152 | } 153 | if streams.Stderr.IsTerminal() { 154 | log.Printf("[TRACE] Stderr is a terminal of width %d", streams.Stderr.Columns()) 155 | } else { 156 | log.Printf("[TRACE] Stderr is not a terminal") 157 | } 158 | if streams.Stdin.IsTerminal() { 159 | log.Printf("[TRACE] Stdin is a terminal") 160 | } else { 161 | log.Printf("[TRACE] Stdin is not a terminal") 162 | } 163 | 164 | // NOTE: We're intentionally calling LoadConfig _before_ handling a possible 165 | // -chdir=... option on the command line, so that a possible relative 166 | // path in the TERRAFORM_CONFIG_FILE environment variable (though probably 167 | // ill-advised) will be resolved relative to the true working directory, 168 | // not the overridden one. 169 | config, diags := cliconfig.LoadConfig() 170 | 171 | if len(diags) > 0 { 172 | // Since we haven't instantiated a command.Meta yet, we need to do 173 | // some things manually here and use some "safe" defaults for things 174 | // that command.Meta could otherwise figure out in smarter ways. 175 | Ui.Error("There are some problems with the CLI configuration:") 176 | for _, diag := range diags { 177 | earlyColor := &colorstring.Colorize{ 178 | Colors: colorstring.DefaultColors, 179 | Disable: true, // Disable color to be conservative until we know better 180 | Reset: true, 181 | } 182 | // We don't currently have access to the source code cache for 183 | // the parser used to load the CLI config, so we can't show 184 | // source code snippets in early diagnostics. 185 | Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78)) 186 | } 187 | if diags.HasErrors() { 188 | Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n") 189 | // We continue to run anyway, since Terraform has reasonable defaults. 190 | } 191 | } 192 | 193 | // Get any configured credentials from the config and initialize 194 | // a service discovery object. The slightly awkward predeclaration of 195 | // disco is required to allow us to pass untyped nil as the creds source 196 | // when creating the source fails. Otherwise we pass a typed nil which 197 | // breaks the nil checks in the disco object 198 | var services *disco.Disco 199 | credsSrc, err := credentialsSource(config) 200 | if err == nil { 201 | services = disco.NewWithCredentialsSource(credsSrc) 202 | } else { 203 | // Most commands don't actually need credentials, and most situations 204 | // that would get us here would already have been reported by the config 205 | // loading above, so we'll just log this one as an aid to debugging 206 | // in the unlikely event that it _does_ arise. 207 | log.Printf("[WARN] Cannot initialize remote host credentials manager: %s", err) 208 | // passing (untyped) nil as the creds source is okay because the disco 209 | // object checks that and just acts as though no credentials are present. 210 | services = disco.NewWithCredentialsSource(nil) 211 | } 212 | services.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 213 | 214 | providerSrc, diags := providerSource(config.ProviderInstallation, services) 215 | if len(diags) > 0 { 216 | Ui.Error("There are some problems with the provider_installation configuration:") 217 | for _, diag := range diags { 218 | earlyColor := &colorstring.Colorize{ 219 | Colors: colorstring.DefaultColors, 220 | Disable: true, // Disable color to be conservative until we know better 221 | Reset: true, 222 | } 223 | Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78)) 224 | } 225 | if diags.HasErrors() { 226 | Ui.Error("As a result of the above problems, Terraform's provider installer may not behave as intended.\n\n") 227 | // We continue to run anyway, because most commands don't do provider installation. 228 | } 229 | } 230 | providerDevOverrides := providerDevOverrides(config.ProviderInstallation) 231 | 232 | // The user can declare that certain providers are being managed on 233 | // Terraform's behalf using this environment variable. This is used 234 | // primarily by the SDK's acceptance testing framework. 235 | unmanagedProviders, err := parseReattachProviders(os.Getenv("TF_REATTACH_PROVIDERS")) 236 | if err != nil { 237 | Ui.Error(err.Error()) 238 | return 1 239 | } 240 | 241 | // Initialize the backends. 242 | backendInit.Init(services) 243 | 244 | // Get the command line args. 245 | binName := filepath.Base(os.Args[0]) 246 | args := os.Args[1:] 247 | 248 | originalWd, err := os.Getwd() 249 | if err != nil { 250 | // It would be very strange to end up here 251 | Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err)) 252 | return 1 253 | } 254 | 255 | // The arguments can begin with a -chdir option to ask Terraform to switch 256 | // to a different working directory for the rest of its work. If that 257 | // option is present then extractChdirOption returns a trimmed args with that option removed. 258 | overrideWd, args, err := extractChdirOption(args) 259 | if err != nil { 260 | Ui.Error(fmt.Sprintf("Invalid -chdir option: %s", err)) 261 | return 1 262 | } 263 | if overrideWd != "" { 264 | err := os.Chdir(overrideWd) 265 | if err != nil { 266 | Ui.Error(fmt.Sprintf("Error handling -chdir option: %s", err)) 267 | return 1 268 | } 269 | } 270 | 271 | // Commands get to hold on to the original working directory here, 272 | // in case they need to refer back to it for any special reason, though 273 | // they should primarily be working with the override working directory 274 | // that we've now switched to above. 275 | 276 | shutdownCh := make(chan struct{}, 2) 277 | shutdownChs[shutdownCh] = struct{}{} 278 | defer func() { 279 | delete(shutdownChs, shutdownCh) 280 | close(shutdownCh) 281 | }() 282 | meta := NewMeta(originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders, shutdownCh) 283 | commands := NewCommands(meta) 284 | 285 | // Run checkpoint 286 | go runCheckpoint(ctx, config) 287 | 288 | // Make sure we clean up any managed plugins at the end of this 289 | defer func() { 290 | plugin.CleanupAndRemoveClients() 291 | }() 292 | 293 | // Build the CLI so far, we do this so we can query the subcommand. 294 | cliRunner := &cli.CLI{ 295 | Args: args, 296 | Commands: commands, 297 | HelpFunc: helpFunc, 298 | HelpWriter: os.Stdout, 299 | } 300 | 301 | // Prefix the args with any args from the EnvCLI 302 | args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args) 303 | if err != nil { 304 | Ui.Error(err.Error()) 305 | return 1 306 | } 307 | 308 | // Prefix the args with any args from the EnvCLI targeting this command 309 | suffix := strings.Replace(strings.Replace( 310 | cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1) 311 | args, err = mergeEnvArgs( 312 | fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args) 313 | if err != nil { 314 | Ui.Error(err.Error()) 315 | return 1 316 | } 317 | 318 | // We shortcut "--version" and "-v" to just show the version 319 | for _, arg := range args { 320 | if arg == "-v" || arg == "-version" || arg == "--version" { 321 | newArgs := make([]string, len(args)+1) 322 | newArgs[0] = "version" 323 | copy(newArgs[1:], args) 324 | args = newArgs 325 | break 326 | } 327 | } 328 | 329 | // Rebuild the CLI with any modified args. 330 | log.Printf("[INFO] CLI command args: %#v", args) 331 | cliRunner = &cli.CLI{ 332 | Name: binName, 333 | Args: args, 334 | Commands: commands, 335 | HelpFunc: helpFunc, 336 | HelpWriter: os.Stdout, 337 | 338 | Autocomplete: true, 339 | AutocompleteInstall: "install-autocomplete", 340 | AutocompleteUninstall: "uninstall-autocomplete", 341 | } 342 | 343 | // Before we continue we'll check whether the requested command is 344 | // actually known. If not, we might be able to suggest an alternative 345 | // if it seems like the user made a typo. 346 | // (This bypasses the built-in help handling in cli.CLI for the situation 347 | // where a command isn't found, because it's likely more helpful to 348 | // mention what specifically went wrong, rather than just printing out 349 | // a big block of usage information.) 350 | 351 | // Check if this is being run via shell auto-complete, which uses the 352 | // binary name as the first argument and won't be listed as a subcommand. 353 | autoComplete := os.Getenv("COMP_LINE") != "" 354 | 355 | if cmd := cliRunner.Subcommand(); cmd != "" && !autoComplete { 356 | // Due to the design of cli.CLI, this special error message only works 357 | // for typos of top-level commands. For a subcommand typo, like 358 | // "terraform state posh", cmd would be "state" here and thus would 359 | // be considered to exist, and it would print out its own usage message. 360 | if _, exists := commands[cmd]; !exists { 361 | suggestions := make([]string, 0, len(commands)) 362 | for name := range commands { 363 | suggestions = append(suggestions, name) 364 | } 365 | suggestion := didyoumean.NameSuggestion(cmd, suggestions) 366 | if suggestion != "" { 367 | suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) 368 | } 369 | fmt.Fprintf(os.Stderr, "Terraform has no command named %q.%s\n\nTo see all of Terraform's top-level commands, run:\n terraform -help\n\n", cmd, suggestion) 370 | return 1 371 | } 372 | } 373 | 374 | exitCode, err := cliRunner.Run() 375 | if err != nil { 376 | Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error())) 377 | return 1 378 | } 379 | 380 | // if we are exiting with a non-zero code, check if it was caused by any 381 | // plugins crashing 382 | if exitCode != 0 { 383 | for _, panicLog := range logging.PluginPanics() { 384 | Ui.Error(panicLog) 385 | } 386 | } 387 | return C.int(exitCode) 388 | } 389 | 390 | func NewMeta( 391 | originalWorkingDir string, 392 | streams *terminal.Streams, 393 | config *cliconfig.Config, 394 | services *disco.Disco, 395 | providerSrc getproviders.Source, 396 | providerDevOverrides map[addrs.Provider]getproviders.PackageLocalDir, 397 | unmanagedProviders map[addrs.Provider]*plugin.ReattachConfig, 398 | shutdownCh <-chan struct{}, 399 | ) command.Meta { 400 | var inAutomation bool 401 | if v := os.Getenv(runningInAutomationEnvName); v != "" { 402 | inAutomation = true 403 | } 404 | 405 | for userHost, hostConfig := range config.Hosts { 406 | host, err := svchost.ForComparison(userHost) 407 | if err != nil { 408 | // We expect the config was already validated by the time we get 409 | // here, so we'll just ignore invalid hostnames. 410 | continue 411 | } 412 | services.ForceHostServices(host, hostConfig.Services) 413 | } 414 | 415 | configDir, err := cliconfig.ConfigDir() 416 | if err != nil { 417 | configDir = "" // No config dir available (e.g. looking up a home directory failed) 418 | } 419 | 420 | wd := WorkingDir(originalWorkingDir, os.Getenv("TF_DATA_DIR")) 421 | 422 | meta := command.Meta{ 423 | WorkingDir: wd, 424 | Streams: streams, 425 | View: views.NewView(streams).SetRunningInAutomation(inAutomation), 426 | 427 | Color: true, 428 | GlobalPluginDirs: globalPluginDirs(), 429 | Ui: Ui, 430 | 431 | Services: services, 432 | BrowserLauncher: webbrowser.NewNativeLauncher(), 433 | 434 | RunningInAutomation: inAutomation, 435 | CLIConfigDir: configDir, 436 | PluginCacheDir: config.PluginCacheDir, 437 | 438 | ShutdownCh: shutdownCh, 439 | 440 | ProviderSource: providerSrc, 441 | ProviderDevOverrides: providerDevOverrides, 442 | UnmanagedProviders: unmanagedProviders, 443 | } 444 | return meta 445 | } 446 | 447 | func NewCommands(meta command.Meta) map[string]cli.CommandFactory { 448 | 449 | // The command list is included in the terraform -help 450 | // output, which is in turn included in the docs at 451 | // website/docs/cli/commands/index.html.markdown; if you 452 | // add, remove or reclassify commands then consider updating 453 | // that to match. 454 | 455 | commands := map[string]cli.CommandFactory{ 456 | "apply": func() (cli.Command, error) { 457 | return &command.ApplyCommand{ 458 | Meta: meta, 459 | }, nil 460 | }, 461 | 462 | "console": func() (cli.Command, error) { 463 | return &command.ConsoleCommand{ 464 | Meta: meta, 465 | }, nil 466 | }, 467 | 468 | "destroy": func() (cli.Command, error) { 469 | return &command.ApplyCommand{ 470 | Meta: meta, 471 | Destroy: true, 472 | }, nil 473 | }, 474 | 475 | "env": func() (cli.Command, error) { 476 | return &command.WorkspaceCommand{ 477 | Meta: meta, 478 | LegacyName: true, 479 | }, nil 480 | }, 481 | 482 | "env list": func() (cli.Command, error) { 483 | return &command.WorkspaceListCommand{ 484 | Meta: meta, 485 | LegacyName: true, 486 | }, nil 487 | }, 488 | 489 | "env select": func() (cli.Command, error) { 490 | return &command.WorkspaceSelectCommand{ 491 | Meta: meta, 492 | LegacyName: true, 493 | }, nil 494 | }, 495 | 496 | "env new": func() (cli.Command, error) { 497 | return &command.WorkspaceNewCommand{ 498 | Meta: meta, 499 | LegacyName: true, 500 | }, nil 501 | }, 502 | 503 | "env delete": func() (cli.Command, error) { 504 | return &command.WorkspaceDeleteCommand{ 505 | Meta: meta, 506 | LegacyName: true, 507 | }, nil 508 | }, 509 | 510 | "fmt": func() (cli.Command, error) { 511 | return &command.FmtCommand{ 512 | Meta: meta, 513 | }, nil 514 | }, 515 | 516 | "get": func() (cli.Command, error) { 517 | return &command.GetCommand{ 518 | Meta: meta, 519 | }, nil 520 | }, 521 | 522 | "graph": func() (cli.Command, error) { 523 | return &command.GraphCommand{ 524 | Meta: meta, 525 | }, nil 526 | }, 527 | 528 | "import": func() (cli.Command, error) { 529 | return &command.ImportCommand{ 530 | Meta: meta, 531 | }, nil 532 | }, 533 | 534 | "init": func() (cli.Command, error) { 535 | return &command.InitCommand{ 536 | Meta: meta, 537 | }, nil 538 | }, 539 | 540 | "login": func() (cli.Command, error) { 541 | return &command.LoginCommand{ 542 | Meta: meta, 543 | }, nil 544 | }, 545 | 546 | "logout": func() (cli.Command, error) { 547 | return &command.LogoutCommand{ 548 | Meta: meta, 549 | }, nil 550 | }, 551 | 552 | "output": func() (cli.Command, error) { 553 | return &command.OutputCommand{ 554 | Meta: meta, 555 | }, nil 556 | }, 557 | 558 | "plan": func() (cli.Command, error) { 559 | return &command.PlanCommand{ 560 | Meta: meta, 561 | }, nil 562 | }, 563 | 564 | "providers": func() (cli.Command, error) { 565 | return &command.ProvidersCommand{ 566 | Meta: meta, 567 | }, nil 568 | }, 569 | 570 | "providers lock": func() (cli.Command, error) { 571 | return &command.ProvidersLockCommand{ 572 | Meta: meta, 573 | }, nil 574 | }, 575 | 576 | "providers mirror": func() (cli.Command, error) { 577 | return &command.ProvidersMirrorCommand{ 578 | Meta: meta, 579 | }, nil 580 | }, 581 | 582 | "providers schema": func() (cli.Command, error) { 583 | return &command.ProvidersSchemaCommand{ 584 | Meta: meta, 585 | }, nil 586 | }, 587 | 588 | "push": func() (cli.Command, error) { 589 | return &command.PushCommand{ 590 | Meta: meta, 591 | }, nil 592 | }, 593 | 594 | "refresh": func() (cli.Command, error) { 595 | return &command.RefreshCommand{ 596 | Meta: meta, 597 | }, nil 598 | }, 599 | 600 | "show": func() (cli.Command, error) { 601 | return &command.ShowCommand{ 602 | Meta: meta, 603 | }, nil 604 | }, 605 | 606 | "taint": func() (cli.Command, error) { 607 | return &command.TaintCommand{ 608 | Meta: meta, 609 | }, nil 610 | }, 611 | 612 | "test": func() (cli.Command, error) { 613 | return &command.TestCommand{ 614 | Meta: meta, 615 | }, nil 616 | }, 617 | 618 | "validate": func() (cli.Command, error) { 619 | return &command.ValidateCommand{ 620 | Meta: meta, 621 | }, nil 622 | }, 623 | 624 | "version": func() (cli.Command, error) { 625 | return &command.VersionCommand{ 626 | Meta: meta, 627 | Version: Version, 628 | VersionPrerelease: VersionPrerelease, 629 | Platform: getproviders.CurrentPlatform, 630 | CheckFunc: commandVersionCheck, 631 | }, nil 632 | }, 633 | 634 | "untaint": func() (cli.Command, error) { 635 | return &command.UntaintCommand{ 636 | Meta: meta, 637 | }, nil 638 | }, 639 | 640 | "workspace": func() (cli.Command, error) { 641 | return &command.WorkspaceCommand{ 642 | Meta: meta, 643 | }, nil 644 | }, 645 | 646 | "workspace list": func() (cli.Command, error) { 647 | return &command.WorkspaceListCommand{ 648 | Meta: meta, 649 | }, nil 650 | }, 651 | 652 | "workspace select": func() (cli.Command, error) { 653 | return &command.WorkspaceSelectCommand{ 654 | Meta: meta, 655 | }, nil 656 | }, 657 | 658 | "workspace show": func() (cli.Command, error) { 659 | return &command.WorkspaceShowCommand{ 660 | Meta: meta, 661 | }, nil 662 | }, 663 | 664 | "workspace new": func() (cli.Command, error) { 665 | return &command.WorkspaceNewCommand{ 666 | Meta: meta, 667 | }, nil 668 | }, 669 | 670 | "workspace delete": func() (cli.Command, error) { 671 | return &command.WorkspaceDeleteCommand{ 672 | Meta: meta, 673 | }, nil 674 | }, 675 | 676 | //----------------------------------------------------------- 677 | // Plumbing 678 | //----------------------------------------------------------- 679 | 680 | "force-unlock": func() (cli.Command, error) { 681 | return &command.UnlockCommand{ 682 | Meta: meta, 683 | }, nil 684 | }, 685 | 686 | "state": func() (cli.Command, error) { 687 | return &command.StateCommand{}, nil 688 | }, 689 | 690 | "state list": func() (cli.Command, error) { 691 | return &command.StateListCommand{ 692 | Meta: meta, 693 | }, nil 694 | }, 695 | 696 | "state rm": func() (cli.Command, error) { 697 | return &command.StateRmCommand{ 698 | StateMeta: command.StateMeta{ 699 | Meta: meta, 700 | }, 701 | }, nil 702 | }, 703 | 704 | "state mv": func() (cli.Command, error) { 705 | return &command.StateMvCommand{ 706 | StateMeta: command.StateMeta{ 707 | Meta: meta, 708 | }, 709 | }, nil 710 | }, 711 | 712 | "state pull": func() (cli.Command, error) { 713 | return &command.StatePullCommand{ 714 | Meta: meta, 715 | }, nil 716 | }, 717 | 718 | "state push": func() (cli.Command, error) { 719 | return &command.StatePushCommand{ 720 | Meta: meta, 721 | }, nil 722 | }, 723 | 724 | "state show": func() (cli.Command, error) { 725 | return &command.StateShowCommand{ 726 | Meta: meta, 727 | }, nil 728 | }, 729 | 730 | "state replace-provider": func() (cli.Command, error) { 731 | return &command.StateReplaceProviderCommand{ 732 | StateMeta: command.StateMeta{ 733 | Meta: meta, 734 | }, 735 | }, nil 736 | }, 737 | } 738 | 739 | PrimaryCommands = []string{ 740 | "init", 741 | "validate", 742 | "plan", 743 | "apply", 744 | "destroy", 745 | } 746 | 747 | HiddenCommands = map[string]struct{}{ 748 | "env": struct{}{}, 749 | "internal-plugin": struct{}{}, 750 | "push": struct{}{}, 751 | } 752 | 753 | return commands 754 | } 755 | 756 | // ********************************************** 757 | // Config 758 | // ********************************************** 759 | 760 | // ShortModule is a container for a set of configuration constructs that are 761 | // evaluated within a common namespace. 762 | // Compared with module, there are fewer non-serializable fields. 763 | type ShortModule struct { 764 | SourceDir string 765 | 766 | CoreVersionConstraints []configs.VersionConstraint 767 | 768 | ActiveExperiments experiments.Set 769 | 770 | Backend *configs.Backend 771 | CloudConfig *configs.CloudConfig 772 | ProviderConfigs map[string]*configs.Provider 773 | ProviderRequirements *configs.RequiredProviders 774 | 775 | Variables map[string]*configs.Variable 776 | Locals map[string]*configs.Local 777 | Outputs map[string]*configs.Output 778 | 779 | ModuleCalls map[string]*configs.ModuleCall 780 | 781 | ManagedResources map[string]*configs.Resource 782 | DataResources map[string]*configs.Resource 783 | 784 | Moved []*configs.Moved 785 | } 786 | 787 | func convertModule(mod *configs.Module) *ShortModule { 788 | shortMod := &ShortModule{ 789 | SourceDir: mod.SourceDir, 790 | CoreVersionConstraints: mod.CoreVersionConstraints, 791 | ActiveExperiments: mod.ActiveExperiments, 792 | Backend: mod.Backend, 793 | CloudConfig: mod.CloudConfig, 794 | ProviderRequirements: mod.ProviderRequirements, 795 | Variables: mod.Variables, 796 | Locals: mod.Locals, 797 | Outputs: mod.Outputs, 798 | ModuleCalls: mod.ModuleCalls, 799 | ManagedResources: mod.ManagedResources, 800 | DataResources: mod.DataResources, 801 | Moved: mod.Moved, 802 | } 803 | return shortMod 804 | } 805 | 806 | //export ConfigLoadConfigDir 807 | func ConfigLoadConfigDir(cPath *C.char) (cMod *C.char, cDiags *C.char, cError *C.char) { 808 | defer func() { 809 | recover() 810 | }() 811 | 812 | parser := configs.NewParser(nil) 813 | path := C.GoString(cPath) 814 | mod, diags := parser.LoadConfigDir(path) 815 | modBytes, err := json.Marshal(convertModule(mod)) 816 | if err != nil { 817 | cMod = C.CString("") 818 | cDiags = C.CString("") 819 | cError = C.CString(err.Error()) 820 | return cMod, cDiags, cError 821 | } 822 | diagsBytes, err := json.Marshal(diags) 823 | if err != nil { 824 | cMod = C.CString(string(modBytes)) 825 | cDiags = C.CString("") 826 | cError = C.CString(err.Error()) 827 | return cMod, cDiags, cError 828 | } 829 | cMod = C.CString(string(modBytes)) 830 | cDiags = C.CString(string(diagsBytes)) 831 | cError = C.CString("") 832 | return cMod, cDiags, cError 833 | } 834 | 835 | // ********************************************** 836 | // Utils 837 | // ********************************************** 838 | 839 | //export Free 840 | func Free(cString *int) { 841 | C.free(unsafe.Pointer(cString)) 842 | } 843 | -------------------------------------------------------------------------------- /libterraform/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ctypes import c_void_p, cdll 3 | 4 | from libterraform.common import WINDOWS 5 | 6 | __version__ = "0.8.0" 7 | 8 | root = os.path.dirname(os.path.abspath(__file__)) 9 | _lib_filename = "libterraform.dll" if WINDOWS else "libterraform.so" 10 | _lib_tf = cdll.LoadLibrary(os.path.join(root, _lib_filename)) 11 | 12 | _free = _lib_tf.Free 13 | _free.argtypes = [c_void_p] 14 | 15 | from .cli import TerraformCommand 16 | from .config import TerraformConfig 17 | 18 | __all__ = ["TerraformCommand", "TerraformConfig"] 19 | -------------------------------------------------------------------------------- /libterraform/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ctypes import * 3 | from threading import Thread 4 | from typing import List, Sequence, Union 5 | 6 | from libterraform import _lib_tf 7 | from libterraform.common import WINDOWS, CmdType, json_loads 8 | from libterraform.exceptions import TerraformCommandError, TerraformFdReadError 9 | 10 | _run_cli = _lib_tf.RunCli 11 | _run_cli.argtypes = [c_int64, POINTER(c_char_p), c_int64, c_int64] 12 | 13 | 14 | def flag(value): 15 | return ... if value else None 16 | 17 | 18 | class CommandResult: 19 | __slots__ = ("retcode", "value", "error", "json") 20 | 21 | def __init__(self, retcode, value, error=None, json=False): 22 | self.retcode = retcode 23 | self.value = value 24 | self.error = error 25 | self.json = json 26 | 27 | def __repr__(self): 28 | return f"" 29 | 30 | 31 | class TerraformCommand: 32 | """Terraform command line. 33 | 34 | https://www.terraform.io/ 35 | """ 36 | 37 | def __init__(self, cwd=None): 38 | self.cwd = cwd 39 | 40 | @classmethod 41 | def run( 42 | cls, 43 | cmd: CmdType, 44 | args: Sequence[str] = None, 45 | options: dict = None, 46 | chdir=None, 47 | check: bool = False, 48 | json=False, 49 | ) -> (int, str, str): 50 | """ 51 | Run command with args and return a tuple (retcode, stdout, stderr). 52 | 53 | The returned object will have attributes retcode, value, json. 54 | 55 | If check is True and the return code was non 0 or 2, it raises a 56 | TerraformCommandError. The TerraformCommandError object will have the return code 57 | in the retcode attribute, and stdout & stderr attributes. 58 | 59 | :param cmd: Terraform command 60 | :param args: Terraform command argument list 61 | :param options: Terraform command options 62 | Each key in options should be snake format, and will be convert to command option key automatically. 63 | ex. no_color will be converted to -no-color. 64 | Each value in options will be converted to appropriate command value automatically. 65 | The conversion rules for values are as follows: 66 | value ... will be regarded as flag option. 67 | ex. {"json": ...} -> -json 68 | boolean value will be converted to lower boolean. 69 | ex. {"backend": True} -> -backend=true 70 | list value will be converted to multi pairs. 71 | ex. {"var": ["Name1=xx", "Name2=xx"]} -> -var Name1=xx -var Name2=xx 72 | :param chdir: Switch to a different working directory before executing the given subcommand. 73 | :param check: Whether to check return code. 74 | :param json: Whether to load stdout as json. Only partial commands support json param. 75 | :return: Command result tuple (retcode, stdout, stderr). 76 | """ 77 | argv = [] 78 | if chdir: 79 | argv.append(f"-chdir={chdir}") 80 | if isinstance(cmd, (list, tuple)): 81 | argv.extend(cmd) 82 | else: 83 | argv.append(cmd) 84 | if json: 85 | options = options if options is not None else {} 86 | options.update(json=flag(json)) 87 | if options is not None: 88 | for option, value in options.items(): 89 | if value is None: 90 | continue 91 | if "_" in option: 92 | option = option.replace("_", "-") 93 | if value is ...: 94 | argv += [f"-{option}"] 95 | continue 96 | if isinstance(value, list): 97 | for val in value: 98 | argv += [f"-{option}={val}"] 99 | continue 100 | if isinstance(value, dict): 101 | for k, v in value.items(): 102 | argv += [f"-{option}={k}={v}"] 103 | continue 104 | if isinstance(value, bool): 105 | value = "true" if value else "false" 106 | argv += [f"-{option}={value}"] 107 | if args: 108 | argv.extend(args) 109 | argc = len(argv) 110 | c_argv = (c_char_p * argc)() 111 | c_argv[:] = [arg.encode("utf-8") for arg in argv] 112 | r_stdout_fd, w_stdout_fd = os.pipe() 113 | r_stderr_fd, w_stderr_fd = os.pipe() 114 | 115 | stdout_buffer = [] 116 | stderr_buffer = [] 117 | stdout_thread = Thread(target=cls._fdread, args=(r_stdout_fd, stdout_buffer)) 118 | stdout_thread.daemon = True 119 | stdout_thread.start() 120 | stderr_thread = Thread(target=cls._fdread, args=(r_stderr_fd, stderr_buffer)) 121 | stderr_thread.daemon = True 122 | stderr_thread.start() 123 | 124 | if WINDOWS: 125 | import msvcrt 126 | 127 | w_stdout_handle = msvcrt.get_osfhandle(w_stdout_fd) 128 | w_stderr_handle = msvcrt.get_osfhandle(w_stderr_fd) 129 | retcode = _run_cli(argc, c_argv, w_stdout_handle, w_stderr_handle) 130 | else: 131 | retcode = _run_cli(argc, c_argv, w_stdout_fd, w_stderr_fd) 132 | 133 | stdout_thread.join() 134 | stderr_thread.join() 135 | if not stdout_buffer: 136 | raise TerraformFdReadError(fd=r_stdout_fd) 137 | if not stderr_buffer: 138 | raise TerraformFdReadError(fd=r_stderr_fd) 139 | stdout = stdout_buffer[0] 140 | stderr = stderr_buffer[0] 141 | 142 | if check and retcode not in (0, 2): 143 | raise TerraformCommandError(retcode, argv, stdout, stderr) 144 | return retcode, stdout, stderr 145 | 146 | @staticmethod 147 | def _fdread(std_fd, std_buffer): 148 | with os.fdopen(std_fd, encoding="utf-8") as std_f: 149 | std = std_f.read() 150 | std_buffer.append(std) 151 | 152 | def version( 153 | self, check: bool = False, json: bool = True, **options 154 | ) -> CommandResult: 155 | """Refer to https://www.terraform.io/docs/commands/version 156 | 157 | Displays the version of Terraform and all installed plugins. 158 | 159 | By default, this assumes you want to get json output 160 | 161 | :param check: Whether to check return code. 162 | :param json: Whether to load stdout as json. 163 | :param options: More command options. 164 | """ 165 | retcode, stdout, stderr = self.run( 166 | "version", options=options, check=check, json=json 167 | ) 168 | value = json_loads(stdout) if json else stdout 169 | return CommandResult(retcode, value, stderr, json) 170 | 171 | def init( 172 | self, 173 | check: bool = False, 174 | backend: bool = None, 175 | backend_config: Union[str, List[str]] = None, 176 | force_copy: bool = None, 177 | from_module: str = None, 178 | get: bool = None, 179 | input: bool = False, 180 | lock: bool = None, 181 | lock_timeout: str = None, 182 | no_color: bool = True, 183 | plugin_dirs: List[str] = None, 184 | reconfigure: bool = None, 185 | migrate_state: bool = None, 186 | upgrade: bool = None, 187 | lockfile: str = None, 188 | ignore_remote_version: bool = None, 189 | test_directory: str = None, 190 | **options, 191 | ) -> CommandResult: 192 | """Refer to https://www.terraform.io/docs/commands/init 193 | 194 | Initialize a new or existing Terraform working directory by creating 195 | initial files, loading any remote state, downloading modules, etc. 196 | 197 | This is the first command that should be run for any new or existing 198 | Terraform configuration per machine. This sets up all the local data 199 | necessary to run Terraform that is typically not committed to version 200 | control. 201 | 202 | This command is always safe to run multiple times. Though subsequent runs 203 | may give errors, this command will never delete your configuration or 204 | state. Even so, if you have important information, please back it up prior 205 | to running this command, just in case. 206 | 207 | By default, this assumes you want to get json output. 208 | 209 | :param check: Whether to check return code. 210 | :param backend: False to disable backend or HCP Terraform initialization 211 | for this configuration and use what was previously initialized instead. 212 | :param backend_config: Configuration to be merged with what is in the 213 | configuration file's 'backend' block. This can be either a path to an 214 | HCL file with key/value assignments (same format as terraform.tfvars) 215 | or a 'key=value' format, and can be specified multiple times. The backend 216 | type must be in the configuration itself 217 | :param force_copy: Suppress prompts about copying state data when initializating 218 | a new state backend. This is equivalent to providing a "yes" to all 219 | confirmation prompts. 220 | :param from_module: Copy the contents of the given module into the target 221 | directory before initialization. 222 | :param get: False to disable downloading modules for this configuration. 223 | :param input: False to disable interactive prompts. Note that some actions may 224 | require interactive prompts and will error if input is disabled. 225 | :param lock: False to not hold a state lock during backend migration. 226 | This is dangerous if others might concurrently run commands against the 227 | same workspace. 228 | :param lock_timeout: Duration to retry a state lock. 229 | :param no_color: True to output not contain any color. 230 | :param plugin_dirs: Directories containing plugin binaries. This overrides all 231 | default search paths for plugins, and prevents the automatic installation 232 | of plugins. 233 | :param reconfigure: Reconfigure a backend, ignoring any saved configuration. 234 | :param migrate_state: Reconfigure a backend, and attempt to migrate any 235 | existing state. 236 | :param upgrade: Install the latest module and provider versions allowed within 237 | configured constraints, overriding the default behavior of selecting exactly 238 | the version recorded in the dependency lockfile. 239 | :param lockfile: Set a dependency lockfile mode. 240 | Currently only "readonly" is valid. 241 | :param ignore_remote_version: A rare option used for HCP Terraform and the 242 | remote backend only. Set this to ignore checking that the local and remote 243 | Terraform versions use compatible state representations, making an operation 244 | proceed even when there is a potential mismatch. 245 | See the documentation on configuring Terraform with 246 | HCP Terraform or Terraform Enterprise for more information. 247 | :param test_directory: Set the Terraform test directory, defaults to "tests". 248 | :param options: More command options. 249 | """ 250 | options.update( 251 | backend=backend, 252 | backend_config=backend_config, 253 | force_copy=flag(force_copy), 254 | from_module=from_module, 255 | get=get, 256 | input=input, 257 | lock=lock, 258 | lock_timeout=lock_timeout, 259 | no_color=flag(no_color), 260 | plugin_dir=plugin_dirs, 261 | reconfigure=flag(reconfigure), 262 | migrate_state=flag(migrate_state), 263 | upgrade=upgrade, 264 | lockfile=lockfile, 265 | ignore_remote_version=flag(ignore_remote_version), 266 | test_directory=test_directory, 267 | ) 268 | retcode, stdout, stderr = self.run( 269 | "init", options=options, chdir=self.cwd, check=check 270 | ) 271 | return CommandResult(retcode, stdout, stderr) 272 | 273 | def validate( 274 | self, 275 | check: bool = False, 276 | json: bool = True, 277 | no_color: bool = True, 278 | no_test: bool = None, 279 | test_directory: str = None, 280 | **options, 281 | ) -> CommandResult: 282 | """Refer to https://www.terraform.io/docs/commands/validate 283 | 284 | Validate the configuration files in a directory, referring only to the 285 | configuration and not accessing any remote services such as remote state, 286 | provider APIs, etc. 287 | 288 | Validate runs checks that verify whether a configuration is syntactically 289 | valid and internally consistent, regardless of any provided variables or 290 | existing state. It is thus primarily useful for general verification of 291 | reusable modules, including correctness of attribute names and value types. 292 | 293 | It is safe to run this command automatically, for example as a post-save 294 | check in a text editor or as a test step for a re-usable module in a CI 295 | system. 296 | 297 | Validation requires an initialized working directory with any referenced 298 | plugins and modules installed. To initialize a working directory for 299 | validation without accessing any configured remote backend, use: 300 | self.init(backend=False) 301 | 302 | To verify configuration in the context of a particular run (a particular 303 | target workspace, input variable values, etc), use the self.plan() 304 | instead, which includes an implied validation check. 305 | 306 | By default, this assumes you want to get json output. 307 | 308 | :param check: Whether to check return code. 309 | :param json: Whether to load stdout as json. 310 | :param no_color: True to output not contain any color. 311 | :param no_test: If specified, Terraform will not validate test files. 312 | :param test_directory: Set the Terraform test directory, defaults to "tests". 313 | :param options: More command options. 314 | """ 315 | options.update( 316 | no_color=flag(no_color), 317 | no_test=flag(no_test), 318 | test_directory=test_directory, 319 | ) 320 | retcode, stdout, stderr = self.run( 321 | "validate", options=options, chdir=self.cwd, check=check, json=json 322 | ) 323 | value = json_loads(stdout) if json else stdout 324 | return CommandResult(retcode, value, stderr, json=json) 325 | 326 | def plan( 327 | self, 328 | check: bool = False, 329 | json: bool = True, 330 | destroy: bool = None, 331 | refresh_only: bool = None, 332 | refresh: bool = None, 333 | replace: Union[str, List[str]] = None, 334 | target: Union[str, List[str]] = None, 335 | vars: dict = None, 336 | var_files: List[str] = None, 337 | compact_warnings: bool = None, 338 | detailed_exitcode: bool = None, 339 | generate_config_out: str = None, 340 | input: bool = False, 341 | lock: bool = None, 342 | lock_timeout: str = None, 343 | no_color: bool = True, 344 | out: str = None, 345 | parallelism: int = None, 346 | state: str = None, 347 | **options, 348 | ) -> CommandResult: 349 | """Refer to https://www.terraform.io/docs/commands/plan 350 | 351 | Generates a speculative execution plan, showing what actions Terraform 352 | would take to apply the current configuration. This command will not 353 | actually perform the planned actions. 354 | 355 | You can optionally save the plan to a file, which you can then pass to 356 | the self.apply() to perform exactly the actions described in the plan. 357 | 358 | By default, this assumes you want to get json output. 359 | 360 | :param check: Whether to check return code. 361 | :param json: Whether to load stdout as json. 362 | :param destroy: Select the "destroy" planning mode, which creates a plan 363 | to destroy all objects currently managed by this Terraform configuration 364 | instead of the usual behavior. 365 | :param refresh_only: Select the "refresh only" planning mode, which checks 366 | whether remote objects still match the outcome of the most recent Terraform 367 | apply but does not propose any actions to undo any changes made outside 368 | of Terraform. 369 | :param refresh: Skip checking for external changes to remote objects while 370 | creating the plan. This can potentially make planning faster, but at 371 | the expense of possibly planning against a stale record of the remote 372 | system state. 373 | :param replace: Force replacement of a particular resource instance using 374 | its resource address. If the plan would've normally produced an update or 375 | no-op action for this instance, Terraform will plan to replace it instead. 376 | You can use this option multiple times to replace more than one object. 377 | :param target: Limit the planning operation to only the given module, resource, 378 | or resource instance and all of its dependencies. You can use this option 379 | multiple times to include more than one object. This is for exceptional 380 | use only. 381 | :param vars: Set variables in the root module of the configuration. 382 | :param var_files: Load variable values from the given files, in addition to 383 | the default files terraform.tfvars and *.auto.tfvars. 384 | :param compact_warnings: If Terraform produces any warnings that are not 385 | accompanied by errors, shows them in a more compact form that includes 386 | only the summary messages. 387 | :param detailed_exitcode: Return detailed exit codes when the command exits. 388 | This will change the meaning of exit codes to: 389 | 0 - Succeeded, diff is empty (no changes) 390 | 1 - Errored 391 | 2 - Succeeded, there is a diff 392 | :param generate_config_out: (Experimental) If import blocks are present in 393 | configuration, instructs Terraform to generate HCL 394 | for any imported resources not already present. The 395 | configuration is written to a new file at PATH, 396 | which must not already exist. Terraform may still 397 | attempt to write configuration if the plan errors. 398 | :param input: False to disable interactive prompts. Note that some actions may 399 | require interactive prompts and will error if input is disabled. 400 | :param lock: False to not hold a state lock during backend migration. 401 | This is dangerous if others might concurrently run commands against the 402 | same workspace. 403 | :param lock_timeout: Duration to retry a state lock. 404 | :param no_color: True to output not contain any color. 405 | :param out: Write a plan file to the given path. This can be used as 406 | input to the show or apply command. 407 | :param parallelism: Limit the number of concurrent operations. Defaults to 10. 408 | :param state: A legacy option used for the local backend only. See the 409 | local backend's documentation for more information. 410 | :param options: More command options. 411 | """ 412 | options.update( 413 | destroy=flag(destroy), 414 | refresh_only=flag(refresh_only), 415 | refresh=refresh, 416 | replace=replace, 417 | target=target, 418 | var=vars, 419 | var_file=var_files, 420 | compact_warnings=flag(compact_warnings), 421 | detailed_exitcode=flag(detailed_exitcode), 422 | generate_config_out=generate_config_out, 423 | input=input, 424 | lock=lock, 425 | lock_timeout=lock_timeout, 426 | no_color=flag(no_color), 427 | out=out, 428 | parallelism=parallelism, 429 | state=state, 430 | ) 431 | retcode, stdout, stderr = self.run( 432 | "plan", options=options, chdir=self.cwd, check=check, json=json 433 | ) 434 | value = json_loads(stdout, split=True) if json else stdout 435 | return CommandResult(retcode, value, stderr, json=json) 436 | 437 | def show( 438 | self, 439 | path: str = None, 440 | check: bool = False, 441 | json: bool = True, 442 | no_color: bool = True, 443 | **options, 444 | ) -> CommandResult: 445 | """Refer to https://www.terraform.io/docs/commands/show 446 | 447 | Reads and outputs a Terraform state or plan file in a human-readable 448 | form. If no path is specified, the current state will be shown. 449 | 450 | By default, this assumes you want to get json output. 451 | 452 | :param path: Terraform state or plan file path. 453 | :param check: Whether to check return code. 454 | :param json: Whether to load stdout as json. 455 | :param no_color: True to output not contain any color. 456 | :param options: More command options. 457 | """ 458 | options.update( 459 | no_color=flag(no_color), 460 | ) 461 | args = [path] if path else None 462 | retcode, stdout, stderr = self.run( 463 | "show", args, options=options, chdir=self.cwd, check=check, json=json 464 | ) 465 | value = json_loads(stdout) if json else stdout 466 | return CommandResult(retcode, value, stderr, json=json) 467 | 468 | def apply( 469 | self, 470 | plan: str = None, 471 | check: bool = False, 472 | json: bool = True, 473 | auto_approve: bool = True, 474 | backup: str = None, 475 | compact_warnings: bool = None, 476 | input: bool = False, 477 | lock: bool = None, 478 | lock_timeout: str = None, 479 | no_color: bool = True, 480 | parallelism: int = None, 481 | state: str = None, 482 | state_out: str = None, 483 | destroy: bool = None, 484 | **options, 485 | ) -> CommandResult: 486 | """Refer to https://www.terraform.io/docs/commands/apply 487 | 488 | Creates or updates infrastructure according to Terraform configuration 489 | files in the current directory. 490 | 491 | By default, Terraform will generate a new plan and present it for your 492 | approval before taking any action. You can optionally provide a plan 493 | file created by a previous call to self.plan(), in which case 494 | Terraform will take the actions described in that plan without any 495 | confirmation prompt. 496 | 497 | If you don't provide a saved plan file then this command will also accept 498 | all of the plan-customization options accepted by the terraform plan command. 499 | 500 | By default, this assumes you want to get json output. 501 | 502 | :param plan: Terraform plan file path. 503 | :param check: Whether to check return code. 504 | :param json: Whether to load stdout as json. 505 | :param auto_approve: Skip interactive approval of plan before applying. 506 | :param backup: Path to backup the existing state file before modifying. 507 | Defaults to the `state_out` path with ".backup" extension. 508 | Set to "-" to disable backup. 509 | :param compact_warnings: If Terraform produces any warnings that are not 510 | accompanied by errors, shows them in a more compact form that includes 511 | only the summary messages. 512 | :param input: False to disable interactive prompts. Note that some actions may 513 | require interactive prompts and will error if input is disabled. 514 | :param lock: False to not hold a state lock during backend migration. 515 | This is dangerous if others might concurrently run commands against the 516 | same workspace. 517 | :param lock_timeout: Duration to retry a state lock. 518 | :param no_color: True to output not contain any color. 519 | :param parallelism: Limit the number of concurrent operations. Defaults to 10. 520 | :param state: Path to read and save state (unless `state_out` is specified). 521 | Defaults to "terraform.tfstate". 522 | :param state_out: Path to write state to that is different than `state`. 523 | This can be used to preserve the old state. 524 | :param destroy: Select the "destroy" planning mode, which creates a plan 525 | to destroy all objects currently managed by this Terraform configuration 526 | instead of the usual behavior. 527 | :param options: More command options. 528 | """ 529 | options.update( 530 | auto_approve=flag(auto_approve), 531 | backup=backup, 532 | compact_warnings=flag(compact_warnings), 533 | input=input, 534 | lock=lock, 535 | lock_timeout=lock_timeout, 536 | no_color=flag(no_color), 537 | parallelism=parallelism, 538 | state=state, 539 | state_out=state_out, 540 | destroy=flag(destroy), 541 | ) 542 | args = [plan] if plan else None 543 | retcode, stdout, stderr = self.run( 544 | "apply", args, options=options, chdir=self.cwd, check=check, json=json 545 | ) 546 | value = json_loads(stdout, split=True) if json else stdout 547 | return CommandResult(retcode, value, stderr, json=json) 548 | 549 | def destroy( 550 | self, 551 | check: bool = False, 552 | json: bool = True, 553 | auto_approve: bool = True, 554 | backup: str = None, 555 | compact_warnings: bool = None, 556 | input: bool = False, 557 | lock: bool = None, 558 | lock_timeout: str = None, 559 | no_color: bool = True, 560 | parallelism: int = None, 561 | state: str = None, 562 | state_out: str = None, 563 | **options, 564 | ) -> CommandResult: 565 | """Refer to https://www.terraform.io/docs/commands/destroy 566 | 567 | Destroy Terraform-managed infrastructure. 568 | 569 | By default, this assumes you want to get json output. 570 | 571 | This command is a convenience alias for: 572 | terraform apply -destroy 573 | 574 | This command also accepts many of the plan-customization options accepted by 575 | the terraform plan command. For more information on those options, run: 576 | terraform plan -help 577 | 578 | :param check: Whether to check return code. 579 | :param json: Whether to load stdout as json. 580 | :param auto_approve: Skip interactive approval of plan before applying. 581 | :param backup: Path to backup the existing state file before modifying. 582 | Defaults to the `state_out` path with ".backup" extension. 583 | Set to "-" to disable backup. 584 | :param compact_warnings: If Terraform produces any warnings that are not 585 | accompanied by errors, shows them in a more compact form that includes 586 | only the summary messages. 587 | :param input: False to disable interactive prompts. Note that some actions may 588 | require interactive prompts and will error if input is disabled. 589 | :param lock: False to not hold a state lock during backend migration. 590 | This is dangerous if others might concurrently run commands against the 591 | same workspace. 592 | :param lock_timeout: Duration to retry a state lock. 593 | :param no_color: True to output not contain any color. 594 | :param parallelism: Limit the number of concurrent operations. Defaults to 10. 595 | :param state: Path to read and save state (unless `state_out` is specified). 596 | Defaults to "terraform.tfstate". 597 | :param state_out: Path to write state to that is different than `state`. 598 | This can be used to preserve the old state. 599 | :param options: More command options. 600 | """ 601 | options.update( 602 | auto_approve=flag(auto_approve), 603 | backup=backup, 604 | compact_warnings=flag(compact_warnings), 605 | input=input, 606 | lock=lock, 607 | lock_timeout=lock_timeout, 608 | no_color=flag(no_color), 609 | parallelism=parallelism, 610 | state=state, 611 | state_out=state_out, 612 | ) 613 | retcode, stdout, stderr = self.run( 614 | "destroy", options=options, chdir=self.cwd, check=check, json=json 615 | ) 616 | value = json_loads(stdout, split=True) if json else stdout 617 | return CommandResult(retcode, value, stderr, json=json) 618 | 619 | def fmt( 620 | self, 621 | dir: Union[str, List[str]] = None, 622 | check: bool = False, 623 | no_color: bool = True, 624 | list: bool = None, 625 | write: bool = None, 626 | diff: bool = None, 627 | check_input: bool = None, 628 | recursive: bool = None, 629 | **options, 630 | ) -> CommandResult: 631 | """Refer to https://www.terraform.io/docs/commands/fmt 632 | 633 | Rewrites all Terraform configuration files to a canonical format. All 634 | configuration files (.tf), variables files (.tfvars), and testing files 635 | (.tftest.hcl) are updated. JSON files (.tf.json, .tfvars.json, or 636 | .tftest.json) are not modified. 637 | 638 | By default, fmt scans the current directory for configuration files. If you 639 | provide a directory for the target argument, then fmt will scan that 640 | directory instead. If you provide a file, then fmt will process just that 641 | file. If you provide a single dash ("-"), then fmt will read from standard 642 | input (STDIN). 643 | 644 | If DIR is not specified then the current working directory will be used. 645 | If DIR is "-" then content will be read from STDIN. The given content must 646 | be in the Terraform language native syntax; JSON is not supported. 647 | 648 | :param dir: Directory which Terraform configuration files located. 649 | :param check: Whether to check return code. 650 | :param no_color: True to output not contain any color. 651 | :param list: False to not list files whose formatting differs 652 | (always disabled if using STDIN) 653 | :param write: False to not write to source files 654 | (always disabled if using STDIN or checkout_input=True) 655 | :param diff: Display diffs of formatting changes 656 | :param check_input: Check if the input is formatted. 657 | Exit status will be 0 if all input is properly formatted and non-zero otherwise. 658 | :param recursive: Also process files in subdirectories. By default, only the 659 | given directory (or current directory) is processed. 660 | :param options: More command options. 661 | """ 662 | options.update( 663 | no_color=flag(no_color), 664 | list=list, 665 | write=write, 666 | diff=flag(diff), 667 | check=flag(check_input), 668 | recursive=flag(recursive), 669 | ) 670 | if dir: 671 | args = dir 672 | if not isinstance(dir, List): 673 | args = [dir] 674 | else: 675 | args = None 676 | retcode, stdout, stderr = self.run( 677 | "fmt", args, options=options, chdir=self.cwd, check=check 678 | ) 679 | return CommandResult(retcode, stdout, stderr, json=False) 680 | 681 | def force_unlock( 682 | self, 683 | lock_id: str, 684 | check: bool = False, 685 | no_color: bool = True, 686 | force: bool = True, 687 | **options, 688 | ) -> CommandResult: 689 | """Refer to https://www.terraform.io/docs/commands/force-unlock 690 | 691 | Manually unlock the state for the defined configuration. 692 | 693 | This will not modify your infrastructure. This command removes the lock on the 694 | state for the current workspace. The behavior of this lock is dependent 695 | on the backend being used. Local state files cannot be unlocked by another 696 | process. 697 | 698 | :param lock_id: Lock ID. 699 | :param check: Whether to check return code. 700 | :param no_color: True to output not contain any color. 701 | :param force: True to not ask for input for unlock confirmation. 702 | :param options: More command options. 703 | """ 704 | options.update( 705 | no_color=flag(no_color), 706 | force=flag(force), 707 | ) 708 | args = [lock_id] 709 | retcode, stdout, stderr = self.run( 710 | "force-unlock", args, options=options, chdir=self.cwd, check=check 711 | ) 712 | return CommandResult(retcode, stdout, stderr, json=False) 713 | 714 | def get( 715 | self, 716 | check: bool = False, 717 | no_color: bool = True, 718 | update: bool = None, 719 | test_directory: str = None, 720 | **options, 721 | ) -> CommandResult: 722 | """Refer to https://www.terraform.io/docs/commands/get 723 | 724 | Downloads and installs modules needed for the configuration in the 725 | current working directory. 726 | 727 | This recursively downloads all modules needed, such as modules 728 | imported by modules imported by the root and so on. If a module is 729 | already downloaded, it will not be redownloaded or checked for updates 730 | unless the -update flag is specified. 731 | 732 | Module installation also happens automatically by default as part of 733 | the "terraform init" command, so you should rarely need to run this 734 | command separately. 735 | 736 | :param check: Whether to check return code. 737 | :param no_color: True to output not contain any color. 738 | :param update: Check already-downloaded modules for available updates 739 | and install the newest versions available. 740 | :param test_directory: Set the Terraform test directory, defaults to "tests". 741 | :param options: More command options. 742 | """ 743 | options.update( 744 | no_color=flag(no_color), 745 | update=flag(update), 746 | test_directory=test_directory, 747 | ) 748 | retcode, stdout, stderr = self.run( 749 | "get", options=options, chdir=self.cwd, check=check 750 | ) 751 | return CommandResult(retcode, stdout, stderr, json=False) 752 | 753 | def graph( 754 | self, 755 | check: bool = False, 756 | no_color: bool = True, 757 | plan: str = None, 758 | draw_cycles: bool = None, 759 | type: str = None, 760 | **options, 761 | ) -> CommandResult: 762 | """Refer to https://www.terraform.io/docs/commands/graph 763 | 764 | Produces a representation of the dependency graph between different 765 | objects in the current configuration and state. 766 | 767 | By default the graph shows a summary only of the relationships between 768 | resources in the configuration, since those are the main objects that 769 | have side-effects whose ordering is significant. You can generate more 770 | detailed graphs reflecting Terraform's actual evaluation strategy 771 | by specifying the -type=TYPE option to select an operation type. 772 | 773 | The graph is presented in the DOT language. The typical program that can 774 | read this format is GraphViz, but many web services are also available 775 | to read this format. 776 | 777 | :param check: Whether to check return code. 778 | :param no_color: True to output not contain any color. 779 | :param plan: Render graph using the specified plan file instead of the 780 | configuration in the current directory. Implies type=apply. 781 | :param draw_cycles: True to highlight any cycles in the graph with colored edges. 782 | This helps when diagnosing cycle errors. This option is 783 | supported only when illustrating a real evaluation graph, 784 | selected using the type=TYPE option. 785 | :param type: Type of operation graph to output. Can be: plan, 786 | plan-refresh-only, plan-destroy, or apply. By default 787 | Terraform just summarizes the relationships between the 788 | resources in your configuration, without any particular 789 | operation in mind. Full operation graphs are more detailed 790 | but therefore often harder to read. 791 | :param options: More command options. 792 | """ 793 | options.update( 794 | no_color=flag(no_color), 795 | plan=plan, 796 | draw_cycles=flag(draw_cycles), 797 | type=type, 798 | ) 799 | retcode, stdout, stderr = self.run( 800 | "graph", options=options, chdir=self.cwd, check=check 801 | ) 802 | return CommandResult(retcode, stdout, stderr, json=False) 803 | 804 | def import_resource( 805 | self, 806 | addr: str, 807 | id: str, 808 | check: bool = False, 809 | config: str = None, 810 | input: bool = False, 811 | lock: bool = None, 812 | lock_timeout: str = None, 813 | no_color: bool = True, 814 | vars: dict = None, 815 | var_files: List[str] = None, 816 | ignore_remote_version: bool = None, 817 | **options, 818 | ) -> CommandResult: 819 | """Refer to https://www.terraform.io/docs/commands/import 820 | 821 | Import existing infrastructure into your Terraform state. 822 | 823 | This will find and import the specified resource into your Terraform 824 | state, allowing existing infrastructure to come under Terraform 825 | management without having to be initially created by Terraform. 826 | 827 | The current implementation of Terraform import can only import resources 828 | into the state. It does not generate configuration. A future version of 829 | Terraform will also generate configuration. 830 | 831 | The ADDR specified is the address to import the resource to. Please 832 | see the documentation online for resource addresses. The ID is a 833 | resource-specific ID to identify that resource being imported. Please 834 | reference the documentation for the resource type you're importing to 835 | determine the ID syntax to use. It typically matches directly to the ID 836 | that the provider uses. 837 | 838 | This command will not modify your infrastructure, but it will make 839 | network requests to inspect parts of your infrastructure relevant to 840 | the resource being imported. 841 | 842 | :param addr: The address to import the resource to. 843 | Please see the documentation online for resource addresses. 844 | :param id: The id is a resource-specific ID to identify that resource being imported. 845 | Please reference the documentation for the resource type you're importing to 846 | determine the ID syntax to use. It typically matches directly to the ID 847 | that the provider uses. 848 | :param check: Whether to check return code. 849 | :param config: Path to a directory of Terraform configuration files 850 | to use to configure the provider. Defaults to pwd. 851 | If no config files are present, they must be provided 852 | via the input prompts or env vars. 853 | :param input: False to disable interactive prompts. Note that some actions may 854 | require interactive prompts and will error if input is disabled. 855 | :param lock: False to not hold a state lock during backend migration. 856 | This is dangerous if others might concurrently run commands against the 857 | same workspace. 858 | :param lock_timeout: Duration to retry a state lock. 859 | :param no_color: True to output not contain any color. 860 | :param vars: Set variables in the Terraform configuration. 861 | This is only useful with the "config" option. 862 | :param var_files: Load variable values from the given files, in addition to 863 | the default files terraform.tfvars and *.auto.tfvars. 864 | :param ignore_remote_version: A rare option used for the remote backend only. 865 | See the remote backend documentation for more information. 866 | :param options: More command options. 867 | """ 868 | options.update( 869 | config=config, 870 | input=input, 871 | lock=lock, 872 | lock_timeout=lock_timeout, 873 | no_color=flag(no_color), 874 | var=vars, 875 | var_file=var_files, 876 | ignore_remote_version=flag(ignore_remote_version), 877 | ) 878 | args = [addr, id] 879 | retcode, stdout, stderr = self.run( 880 | "import", args, options=options, chdir=self.cwd, check=check 881 | ) 882 | return CommandResult(retcode, stdout, stderr, json=False) 883 | 884 | def output( 885 | self, 886 | name: str = None, 887 | check: bool = False, 888 | json: bool = True, 889 | no_color: bool = True, 890 | state: str = None, 891 | raw: bool = None, 892 | **options, 893 | ) -> CommandResult: 894 | """Refer to https://www.terraform.io/docs/commands/output 895 | 896 | Reads an output variable from a Terraform state file and prints 897 | the value. With no additional arguments, output will display all 898 | the outputs for the root module. If name is not specified, all 899 | outputs are printed. 900 | 901 | :param name: Name of output variable. 902 | :param check: Whether to check return code. 903 | :param json: Whether to load stdout as json. 904 | :param no_color: True to output not contain any color. 905 | :param state: Path to the state file to read. Defaults to "terraform.tfstate". 906 | Ignored when remote state is used. 907 | :param raw: For value types that can be automatically converted to a string, 908 | will print the raw string directly, rather than a human-oriented 909 | representation of the value. 910 | :param options: More command options. 911 | """ 912 | options.update( 913 | no_color=flag(no_color), 914 | state=state, 915 | raw=flag(raw), 916 | ) 917 | args = [name] if name else None 918 | retcode, stdout, stderr = self.run( 919 | "output", args, options=options, chdir=self.cwd, check=check, json=json 920 | ) 921 | value = json_loads(stdout) if json else stdout 922 | return CommandResult(retcode, value, stderr, json=json) 923 | 924 | def providers( 925 | self, 926 | subcmd: str = None, 927 | args: Sequence[str] = None, 928 | check: bool = False, 929 | no_color: bool = True, 930 | json: bool = False, 931 | test_directory: str = None, 932 | **options, 933 | ) -> CommandResult: 934 | """Refer to https://www.terraform.io/docs/commands/providers 935 | 936 | Prints out a tree of modules in the referenced configuration annotated with 937 | their provider requirements. 938 | 939 | This provides an overview of all of the provider requirements across all 940 | referenced modules, as an aid to understanding why particular provider 941 | plugins are needed and why particular versions are selected. 942 | 943 | :param subcmd: Sub commands: lock, mirror and schema. 944 | :param args: Args for command. 945 | :param check: Whether to check return code. 946 | :param no_color: True to output not contain any color. 947 | :param json: Whether to load stdout as json. Only valid when subcmd=schema. 948 | :param test_directory: Set the Terraform test directory, defaults to "tests". 949 | :param options: More command options. 950 | """ 951 | options.update( 952 | no_color=flag(no_color), 953 | test_directory=test_directory, 954 | ) 955 | cmd = ["providers"] 956 | if subcmd: 957 | cmd.append(subcmd) 958 | retcode, stdout, stderr = self.run( 959 | cmd, args=args, options=options, chdir=self.cwd, check=check, json=json 960 | ) 961 | value = json_loads(stdout) if json else stdout 962 | return CommandResult(retcode, value, stderr, json=json) 963 | 964 | def providers_lock( 965 | self, 966 | *providers, 967 | check: bool = False, 968 | no_color: bool = True, 969 | fs_mirror: str = None, 970 | net_mirror: str = None, 971 | platform: Union[str, List[str]] = None, 972 | enable_plugin_cache: bool = False, 973 | **options, 974 | ) -> CommandResult: 975 | """Refer to https://www.terraform.io/docs/commands/providers/lock 976 | 977 | Normally the dependency lock file (.terraform.lock.hcl) is updated 978 | automatically by "terraform init", but the information available to the 979 | normal provider installer can be constrained when you're installing providers 980 | from filesystem or network mirrors, and so the generated lock file can end 981 | up incomplete. 982 | 983 | The "providers lock" subcommand addresses that by updating the lock file 984 | based on the official packages available in the origin registry, ignoring 985 | the currently-configured installation strategy. 986 | 987 | After this command succeeds, the lock file will contain suitable checksums 988 | to allow installation of the providers needed by the current configuration 989 | on all of the selected platforms. 990 | 991 | By default, this command updates the lock file for every provider declared 992 | in the configuration. You can override that behavior by providing one or 993 | more provider source addresses on the command line. 994 | 995 | :param check: Whether to check return code. 996 | :param no_color: True to output not contain any color. 997 | :param fs_mirror: Consult the given filesystem mirror directory instead of 998 | the origin registry for each of the given providers. 999 | This would be necessary to generate lock file entries for a provider 1000 | that is available only via a mirror, and not published in an upstream registry. 1001 | In this case, the set of valid checksums will be limited only to what Terraform 1002 | can learn from the data in the mirror directory. 1003 | :param net_mirror: Consult the given network mirror (given as a base URL) 1004 | instead of the origin registry for each of the given providers. 1005 | This would be necessary to generate lock file entries for a provider 1006 | that is available only via a mirror, and not published in an upstream registry. 1007 | In this case, the set of valid checksums will be limited only to what Terraform 1008 | can learn from the data in the mirror indices. 1009 | :param platform: Choose a target platform to request package checksums for. 1010 | By default, Terraform will request package checksums suitable only for 1011 | the platform where you run this command. Use this option multiple times 1012 | to include checksums for multiple target systems. 1013 | Target names consist of an operating system and a CPU architecture. For example, 1014 | "linux_amd64" selects the Linux operating system running on an AMD64 or x86_64 CPU. 1015 | Each provider is available only for a limited set of target platforms. 1016 | :param enable_plugin_cache: Enable the usage of the globally configured plugin cache. 1017 | This will speed up the locking process, but the providers 1018 | wont be loaded from an authoritative source. 1019 | :param options: More command options. 1020 | """ 1021 | options.update( 1022 | fs_mirror=fs_mirror, 1023 | net_mirror=net_mirror, 1024 | platform=platform, 1025 | enable_plugin_cache=flag(enable_plugin_cache), 1026 | ) 1027 | return self.providers( 1028 | subcmd="lock", args=providers, check=check, no_color=no_color, **options 1029 | ) 1030 | 1031 | def providers_mirror( 1032 | self, 1033 | target_dir: str, 1034 | check: bool = False, 1035 | no_color: bool = True, 1036 | platform: Union[str, List[str]] = None, 1037 | **options, 1038 | ) -> CommandResult: 1039 | """Refer to https://www.terraform.io/docs/commands/providers/mirror 1040 | 1041 | Populates a local directory with copies of the provider plugins needed for 1042 | the current configuration, so that the directory can be used either directly 1043 | as a filesystem mirror or as the basis for a network mirror and thus obtain 1044 | those providers without access to their origin registries in the future. 1045 | 1046 | The mirror directory will contain JSON index files that can be published 1047 | along with the mirrored packages on a static HTTP file server to produce 1048 | a network mirror. Those index files will be ignored if the directory is 1049 | used instead as a local filesystem mirror. 1050 | 1051 | :param target_dir: Choose which target directory to build a mirror for. 1052 | :param check: Whether to check return code. 1053 | :param no_color: True to output not contain any color. 1054 | :param platform: Choose which target platform to build a mirror for. 1055 | By default, Terraform will obtain plugin packages suitable for the 1056 | platform where you run this command. 1057 | Use this flag multiple times to include packages for multiple target systems. 1058 | Target names consist of an operating system and a CPU architecture. 1059 | For example, "linux_amd64" selects the Linux operating system running 1060 | on an AMD64 or x86_64 CPU. Each provider is available only for a limited 1061 | set of target platforms. 1062 | :param options: More command options. 1063 | """ 1064 | options.update( 1065 | platform=platform, 1066 | ) 1067 | args = [target_dir] 1068 | return self.providers( 1069 | subcmd="mirror", args=args, check=check, no_color=no_color, **options 1070 | ) 1071 | 1072 | def providers_schema( 1073 | self, 1074 | check: bool = False, 1075 | no_color: bool = True, 1076 | **options, 1077 | ) -> CommandResult: 1078 | """Refer to https://www.terraform.io/docs/commands/providers 1079 | 1080 | Prints out a json representation of the schemas for all providers used 1081 | in the current configuration. 1082 | 1083 | :param check: Whether to check return code. 1084 | :param no_color: True to output not contain any color. 1085 | :param options: More command options. 1086 | """ 1087 | return self.providers( 1088 | subcmd="schema", check=check, no_color=no_color, json=True, **options 1089 | ) 1090 | 1091 | def refresh( 1092 | self, 1093 | check: bool = False, 1094 | json: bool = True, 1095 | target: Union[str, List[str]] = None, 1096 | vars: dict = None, 1097 | var_files: List[str] = None, 1098 | compact_warnings: bool = None, 1099 | input: bool = False, 1100 | lock: bool = None, 1101 | lock_timeout: str = None, 1102 | no_color: bool = True, 1103 | parallelism: int = None, 1104 | **options, 1105 | ) -> CommandResult: 1106 | """Refer to https://www.terraform.io/docs/commands/refresh 1107 | 1108 | Update the state file of your infrastructure with metadata that matches 1109 | the physical resources they are tracking. 1110 | 1111 | This will not modify your infrastructure, but it can modify your 1112 | state file to update metadata. This metadata might cause new changes 1113 | to occur when you generate a plan or call apply next. 1114 | 1115 | :param check: Whether to check return code. 1116 | :param json: Whether to load stdout as json. 1117 | :param target: Resource to target. Operation will be limited to this resource and 1118 | its dependencies. This flag can be used multiple times. 1119 | :param vars: Set variables in the Terraform configuration. 1120 | :param var_files: Load variable values from the given files, in addition to 1121 | the default files terraform.tfvars and *.auto.tfvars. 1122 | :param compact_warnings: If Terraform produces any warnings that are not 1123 | accompanied by errors, shows them in a more compact form that includes 1124 | only the summary messages. 1125 | :param input: False to disable interactive prompts. Note that some actions may 1126 | require interactive prompts and will error if input is disabled. 1127 | :param lock: False to not hold a state lock during backend migration. 1128 | This is dangerous if others might concurrently run commands against the 1129 | same workspace. 1130 | :param lock_timeout: Duration to retry a state lock. 1131 | :param no_color: True to output not contain any color. 1132 | :param parallelism: Limit the number of concurrent operations. Defaults to 10. 1133 | :param options: More command options. 1134 | """ 1135 | options.update( 1136 | target=target, 1137 | var=vars, 1138 | var_file=var_files, 1139 | compact_warnings=flag(compact_warnings), 1140 | input=input, 1141 | lock=lock, 1142 | lock_timeout=lock_timeout, 1143 | no_color=flag(no_color), 1144 | parallelism=parallelism, 1145 | ) 1146 | retcode, stdout, stderr = self.run( 1147 | "refresh", options=options, chdir=self.cwd, check=check, json=json 1148 | ) 1149 | value = json_loads(stdout, split=True) if json else stdout 1150 | return CommandResult(retcode, value, stderr, json=json) 1151 | 1152 | def state( 1153 | self, 1154 | subcmd: str, 1155 | args: Sequence[str] = None, 1156 | check: bool = False, 1157 | no_color: bool = True, 1158 | json: bool = False, 1159 | **options, 1160 | ) -> CommandResult: 1161 | """Refer to https://www.terraform.io/docs/commands/state 1162 | 1163 | This command has subcommands for advanced state management. 1164 | 1165 | These subcommands can be used to slice and dice the Terraform state. 1166 | This is sometimes necessary in advanced cases. For your safety, all 1167 | state management commands that modify the state create a timestamped 1168 | backup of the state prior to making modifications. 1169 | 1170 | The structure and output of the commands is specifically tailored to work 1171 | well with the common Unix utilities such as grep, awk, etc. We recommend 1172 | using those tools to perform more advanced state tasks. 1173 | 1174 | :param subcmd: Sub commands: list, mv, pull, push, replace-provider, rm and show. 1175 | :param args: Args for command. 1176 | :param check: Whether to check return code. 1177 | :param no_color: True to output not contain any color. 1178 | :param json: Whether to load stdout as json. 1179 | :param options: More command options. 1180 | """ 1181 | options.update( 1182 | no_color=flag(no_color), 1183 | ) 1184 | cmd = ["state", subcmd] 1185 | retcode, stdout, stderr = self.run( 1186 | cmd, args=args, options=options, chdir=self.cwd, check=check, json=json 1187 | ) 1188 | value = json_loads(stdout) if json else stdout 1189 | return CommandResult(retcode, value, stderr, json=json) 1190 | 1191 | def state_list( 1192 | self, 1193 | *addrs, 1194 | check: bool = False, 1195 | no_color: bool = True, 1196 | state: str = None, 1197 | ids: Sequence[str] = None, 1198 | **options, 1199 | ): 1200 | """Refer to https://www.terraform.io/docs/commands/state/list 1201 | 1202 | List resources in the Terraform state. 1203 | 1204 | An error will be returned if any of the resources or modules given as 1205 | filter addresses do not exist in the state. 1206 | 1207 | :param addrs: Can be used to filter the instances by resource or module. 1208 | If no pattern is given, all resource instances are listed. 1209 | The addresses must either be module addresses or absolute resource 1210 | addresses, such as: 1211 | aws_instance.example 1212 | module.example 1213 | module.example.module.child 1214 | module.example.aws_instance.example 1215 | :param check: Whether to check return code. 1216 | :param no_color: True to output not contain any color. 1217 | :param state: Path to a Terraform state file to use to look up 1218 | Terraform-managed resources. By default, Terraform will consult 1219 | the state of the currently-selected workspace. 1220 | :param ids: Filters the results to include only instances whose 1221 | resource types have an attribute named "id" whose value is in 1222 | the given ids. 1223 | :param options: More command options. 1224 | """ 1225 | options.update(id=ids) 1226 | return self.state( 1227 | "list", args=addrs, check=check, no_color=no_color, state=state, **options 1228 | ) 1229 | 1230 | def state_mv( 1231 | self, 1232 | src: str, 1233 | dst: str, 1234 | check: bool = False, 1235 | no_color: bool = True, 1236 | dry_run: bool = None, 1237 | lock: bool = None, 1238 | lock_timeout: str = None, 1239 | ignore_remote_version: bool = None, 1240 | **options, 1241 | ): 1242 | """Refer to https://www.terraform.io/docs/commands/state/mv 1243 | 1244 | This command will move an item matched by the address given to the 1245 | destination address. This command can also move to a destination address 1246 | in a completely different state file. 1247 | 1248 | This can be used for simple resource renaming, moving items to and from 1249 | a module, moving entire modules, and more. And because this command can also 1250 | move data to a completely new state, it can also be used for refactoring 1251 | one configuration into multiple separately managed Terraform configurations. 1252 | 1253 | This command will output a backup copy of the state prior to saving any 1254 | changes. The backup cannot be disabled. Due to the destructive nature 1255 | of this command, backups are required. 1256 | 1257 | If you're moving an item to a different state file, a backup will be created 1258 | for each state file. 1259 | 1260 | :param src: Source address of resource. 1261 | :param dst: Destination address of resource. 1262 | :param check: Whether to check return code. 1263 | :param no_color: True to output not contain any color. 1264 | :param dry_run: True to print out what would've been moved but doesn't 1265 | actually move anything. 1266 | :param lock: False to not hold a state lock during backend migration. 1267 | This is dangerous if others might concurrently run commands against the 1268 | same workspace. 1269 | :param lock_timeout: Duration to retry a state lock. 1270 | :param ignore_remote_version: A rare option used for the remote backend only. See 1271 | the remote backend documentation for more information. 1272 | :param options: More command options. 1273 | """ 1274 | options.update( 1275 | dry_run=flag(dry_run), 1276 | lock=lock, 1277 | lock_timeout=lock_timeout, 1278 | ignore_remote_version=flag(ignore_remote_version), 1279 | ) 1280 | return self.state( 1281 | "mv", args=[src, dst], check=check, no_color=no_color, **options 1282 | ) 1283 | 1284 | def state_pull( 1285 | self, 1286 | check: bool = False, 1287 | no_color: bool = True, 1288 | **options, 1289 | ): 1290 | """Refer to https://www.terraform.io/docs/commands/state/pull 1291 | 1292 | Pull the state from its location, upgrade the local copy, and output it. 1293 | As part of this process, Terraform will upgrade the state format of the 1294 | local copy to the current version. 1295 | 1296 | The primary use of this is for state stored remotely. This command 1297 | will still work with local state but is less useful for this. 1298 | 1299 | :param check: Whether to check return code. 1300 | :param no_color: True to output not contain any color. 1301 | :param options: More command options. 1302 | """ 1303 | options.update( 1304 | no_color=flag(no_color), 1305 | ) 1306 | cmd = ["state", "pull"] 1307 | retcode, stdout, stderr = self.run( 1308 | cmd, options=options, chdir=self.cwd, check=check 1309 | ) 1310 | json = retcode == 0 1311 | value = json_loads(stdout) if json else stdout 1312 | return CommandResult(retcode, value, stderr, json=json) 1313 | 1314 | def state_push( 1315 | self, 1316 | path: str, 1317 | check: bool = False, 1318 | no_color: bool = True, 1319 | force: bool = None, 1320 | lock: bool = None, 1321 | lock_timeout: str = None, 1322 | **options, 1323 | ): 1324 | """Refer to https://www.terraform.io/docs/commands/state/push 1325 | 1326 | Update remote state from a local state file at path. 1327 | The command will protect you against writing an older serial or a 1328 | different state file lineage unless you specify the"force" flag. 1329 | 1330 | This command works with local state (it will overwrite the local 1331 | state), but is less useful for this use case. 1332 | 1333 | If PATH is "-", then this command will read the state to push from stdin. 1334 | Data from stdin is not streamed to the backend: it is loaded completely 1335 | (until pipe close), verified, and then pushed. 1336 | 1337 | :param path: The path of the local state file. 1338 | :param check: Whether to check return code. 1339 | :param force: True to write the state even if lineages don't match or the 1340 | remote serial is higher. 1341 | :param no_color: True to output not contain any color. 1342 | :param lock: False to not hold a state lock during backend migration. 1343 | This is dangerous if others might concurrently run commands against the 1344 | same workspace. 1345 | :param lock_timeout: Duration to retry a state lock. 1346 | :param options: More command options. 1347 | """ 1348 | options.update( 1349 | force=flag(force), 1350 | lock=lock, 1351 | lock_timeout=lock_timeout, 1352 | ) 1353 | return self.state( 1354 | "push", args=[path], check=check, no_color=no_color, **options 1355 | ) 1356 | 1357 | def state_replace_provider( 1358 | self, 1359 | from_provider: str, 1360 | to_provider: str, 1361 | check: bool = False, 1362 | no_color: bool = True, 1363 | auto_approve: bool = True, 1364 | lock: bool = None, 1365 | lock_timeout: str = None, 1366 | ignore_remote_version: bool = None, 1367 | **options, 1368 | ): 1369 | """Refer to https://www.terraform.io/cli/commands/state/replace-provider 1370 | 1371 | Replace provider for resources in the Terraform state. 1372 | 1373 | :param from_provider: FROM_PROVIDER_FQN. 1374 | :param to_provider: TO_PROVIDER_FQN. 1375 | :param check: Whether to check return code. 1376 | :param no_color: True to output not contain any color. 1377 | :param auto_approve: Skip interactive approval. 1378 | :param lock: False to not hold a state lock during backend migration. 1379 | This is dangerous if others might concurrently run commands against the 1380 | same workspace. 1381 | :param lock_timeout: Duration to retry a state lock. 1382 | :param ignore_remote_version: A rare option used for the remote backend only. See 1383 | the remote backend documentation for more information. 1384 | :param options: More command options. 1385 | """ 1386 | options.update( 1387 | lock=lock, 1388 | lock_timeout=lock_timeout, 1389 | auto_approve=flag(auto_approve), 1390 | ignore_remote_version=flag(ignore_remote_version), 1391 | ) 1392 | return self.state( 1393 | "replace-provider", 1394 | args=[from_provider, to_provider], 1395 | check=check, 1396 | no_color=no_color, 1397 | **options, 1398 | ) 1399 | 1400 | def state_rm( 1401 | self, 1402 | *addrs, 1403 | check: bool = False, 1404 | no_color: bool = True, 1405 | dry_run: bool = None, 1406 | backup: str = None, 1407 | lock: bool = None, 1408 | lock_timeout: str = None, 1409 | state: str = None, 1410 | ignore_remote_version: bool = None, 1411 | **options, 1412 | ): 1413 | """Refer to https://www.terraform.io/cli/commands/state/rm 1414 | 1415 | Remove one or more items from the Terraform state, causing Terraform to 1416 | "forget" those items without first destroying them in the remote system. 1417 | 1418 | This command removes one or more resource instances from the Terraform state 1419 | based on the addresses given. You can view and list the available instances 1420 | with "terraform state list". 1421 | 1422 | If you give the address of an entire module then all of the instances in 1423 | that module and any of its child modules will be removed from the state. 1424 | 1425 | If you give the address of a resource that has "count" or "for_each" set, 1426 | all of the instances of that resource will be removed from the state. 1427 | 1428 | :param addrs: The address list of resources. 1429 | :param check: Whether to check return code. 1430 | :param no_color: True to output not contain any color. 1431 | :param dry_run: Path where Terraform should write the backup state. 1432 | :param backup: True to print out what would've been moved but doesn't 1433 | actually move anything. 1434 | :param lock: False to not hold a state lock during backend migration. 1435 | This is dangerous if others might concurrently run commands against the 1436 | same workspace. 1437 | :param lock_timeout: Duration to retry a state lock. 1438 | :param state: Path to the state file to update. Defaults to the current 1439 | workspace state. 1440 | :param ignore_remote_version: Continue even if remote and local Terraform 1441 | versions are incompatible. This may result in an unusable workspace, 1442 | and should be used with extreme caution. 1443 | :param options: More command options. 1444 | """ 1445 | options.update( 1446 | dry_run=flag(dry_run), 1447 | backup=backup, 1448 | lock=lock, 1449 | lock_timeout=lock_timeout, 1450 | state=state, 1451 | ignore_remote_version=flag(ignore_remote_version), 1452 | ) 1453 | return self.state("rm", args=addrs, check=check, no_color=no_color, **options) 1454 | 1455 | def state_show( 1456 | self, 1457 | addr: str, 1458 | check: bool = False, 1459 | no_color: bool = True, 1460 | state: str = None, 1461 | **options, 1462 | ): 1463 | """Refer to https://www.terraform.io/cli/commands/state/show 1464 | 1465 | Shows the attributes of a resource in the Terraform state. 1466 | 1467 | This command shows the attributes of a single resource in the Terraform 1468 | state. The address argument must be used to specify a single resource. 1469 | You can view the list of available resources with "terraform state list". 1470 | 1471 | :param addr: The address of resource. 1472 | :param check: Whether to check return code. 1473 | :param no_color: True to output not contain any color. 1474 | :param state: Path to the state file to update. Defaults to the current 1475 | workspace state. 1476 | :param options: More command options. 1477 | """ 1478 | options.update( 1479 | state=state, 1480 | ) 1481 | return self.state( 1482 | "show", args=[addr], check=check, no_color=no_color, **options 1483 | ) 1484 | 1485 | def taint( 1486 | self, 1487 | addr: str, 1488 | check: bool = False, 1489 | no_color: bool = True, 1490 | allow_missing_config: bool = None, 1491 | lock: bool = None, 1492 | lock_timeout: str = None, 1493 | ignore_remote_version: bool = None, 1494 | **options, 1495 | ): 1496 | """Refer to https://www.terraform.io/cli/commands/taint 1497 | 1498 | Terraform uses the term "tainted" to describe a resource instance 1499 | which may not be fully functional, either because its creation 1500 | partially failed or because you've manually marked it as such using 1501 | this command. 1502 | 1503 | This will not modify your infrastructure directly, but subsequent 1504 | Terraform plans will include actions to destroy the remote object 1505 | and create a new object to replace it. 1506 | 1507 | You can remove the "taint" state from a resource instance using 1508 | the "terraform untaint" command. 1509 | 1510 | The address is in the usual resource address syntax, such as: 1511 | aws_instance.foo 1512 | aws_instance.bar[1] 1513 | module.foo.module.bar.aws_instance.baz 1514 | 1515 | Use your shell's quoting or escaping syntax to ensure that the 1516 | address will reach Terraform correctly, without any special 1517 | interpretation. 1518 | 1519 | :param addr: The address of resource. 1520 | :param check: Whether to check return code. 1521 | :param no_color: True to output not contain any color. 1522 | :param allow_missing_config: True to regard the command will succeed (exit code 0) 1523 | even if the resource is missing. 1524 | :param lock: False to not hold a state lock during backend migration. 1525 | This is dangerous if others might concurrently run commands against the 1526 | same workspace. 1527 | :param lock_timeout: Duration to retry a state lock. 1528 | :param ignore_remote_version: A rare option used for the remote backend only. See 1529 | the remote backend documentation for more information. 1530 | :param options: More command options. 1531 | """ 1532 | options.update( 1533 | no_color=flag(no_color), 1534 | allow_missing_config=flag(allow_missing_config), 1535 | lock=lock, 1536 | lock_timeout=lock_timeout, 1537 | ignore_remote_version=flag(ignore_remote_version), 1538 | ) 1539 | retcode, stdout, stderr = self.run( 1540 | "taint", args=[addr], options=options, chdir=self.cwd, check=check 1541 | ) 1542 | return CommandResult(retcode, stdout, stderr) 1543 | 1544 | def untaint( 1545 | self, 1546 | addr: str, 1547 | check: bool = False, 1548 | no_color: bool = True, 1549 | allow_missing_config: bool = None, 1550 | lock: bool = None, 1551 | lock_timeout: str = None, 1552 | ignore_remote_version: bool = None, 1553 | **options, 1554 | ): 1555 | """Refer to https://www.terraform.io/cli/commands/untaint 1556 | 1557 | Terraform uses the term "tainted" to describe a resource instance 1558 | which may not be fully functional, either because its creation 1559 | partially failed or because you've manually marked it as such using 1560 | the "terraform taint" command. 1561 | 1562 | This command removes that state from a resource instance, causing 1563 | Terraform to see it as fully-functional and not in need of 1564 | replacement. 1565 | 1566 | This will not modify your infrastructure directly. It only avoids 1567 | Terraform planning to replace a tainted instance in a future operation. 1568 | 1569 | :param addr: The address of resource. 1570 | :param check: Whether to check return code. 1571 | :param no_color: True to output not contain any color. 1572 | :param allow_missing_config: True to regard the command will succeed (exit code 0) 1573 | even if the resource is missing. 1574 | :param lock: False to not hold a state lock during backend migration. 1575 | This is dangerous if others might concurrently run commands against the 1576 | same workspace. 1577 | :param lock_timeout: Duration to retry a state lock. 1578 | :param ignore_remote_version: A rare option used for the remote backend only. See 1579 | the remote backend documentation for more information. 1580 | :param options: More command options. 1581 | """ 1582 | options.update( 1583 | no_color=flag(no_color), 1584 | allow_missing_config=flag(allow_missing_config), 1585 | lock=lock, 1586 | lock_timeout=lock_timeout, 1587 | ignore_remote_version=flag(ignore_remote_version), 1588 | ) 1589 | retcode, stdout, stderr = self.run( 1590 | "untaint", args=[addr], options=options, chdir=self.cwd, check=check 1591 | ) 1592 | return CommandResult(retcode, stdout, stderr) 1593 | 1594 | def test( 1595 | self, 1596 | check: bool = False, 1597 | vars: dict = None, 1598 | var_files: List[str] = None, 1599 | no_color: bool = True, 1600 | cloud_run: str = None, 1601 | filter: Union[str, List[str]] = None, 1602 | json: bool = True, 1603 | test_directory: str = None, 1604 | verbose: bool = None, 1605 | **options, 1606 | ): 1607 | """Refer to https://www.terraform.io/cli/commands/test 1608 | 1609 | Executes automated integration tests against the current Terraform 1610 | configuration. 1611 | 1612 | Terraform will search for .tftest.hcl files within the current configuration 1613 | and testing directories. Terraform will then execute the testing run blocks 1614 | within any testing files in order, and verify conditional checks and 1615 | assertions against the created infrastructure. 1616 | 1617 | This command creates real infrastructure and will attempt to clean up the 1618 | testing infrastructure on completion. Monitor the output carefully to ensure 1619 | this cleanup process is successful. 1620 | 1621 | By default, this assumes you want to get json output. 1622 | 1623 | :param check: Whether to check return code. 1624 | :param vars: Set variables in the root module of the configuration. 1625 | :param var_files: Load variable values from the given file, in addition 1626 | to the default files terraform.tfvars and *.auto.tfvars. 1627 | :param no_color: True to output not contain any color. 1628 | :param cloud_run: If specified, Terraform will execute this test run 1629 | remotely using HCP Terraform or Terraform Enterpise. 1630 | You must specify the source of a module registered in 1631 | a private module registry as the argument to this flag. 1632 | This allows Terraform to associate the cloud run with 1633 | the correct HCP Terraform or Terraform Enterprise module 1634 | and organization. 1635 | :param json: Whether to load stdout as json. 1636 | :param test_directory: Set the Terraform test directory, defaults to "tests". 1637 | :param verbose: Print the plan or state for each test run block as it 1638 | executes. 1639 | :param options: More command options. 1640 | """ 1641 | options.update( 1642 | var=vars, 1643 | var_file=var_files, 1644 | no_color=flag(no_color), 1645 | cloud_run=cloud_run, 1646 | filter=filter, 1647 | test_directory=test_directory, 1648 | verbose=flag(verbose), 1649 | ) 1650 | retcode, stdout, stderr = self.run( 1651 | "test", options=options, chdir=self.cwd, check=check, json=json 1652 | ) 1653 | value = json_loads(stdout, split=True) if json else stdout 1654 | return CommandResult(retcode, value, stderr, json=json) 1655 | 1656 | def workspace( 1657 | self, 1658 | subcmd: str, 1659 | args: Sequence[str] = None, 1660 | check: bool = False, 1661 | no_color: bool = True, 1662 | **options, 1663 | ) -> CommandResult: 1664 | """Refer to https://www.terraform.io/docs/commands/workspace 1665 | 1666 | new, list, show, select and delete Terraform workspaces. 1667 | 1668 | :param subcmd: Sub commands: list, mv, pull, push, replace-provider, rm and show. 1669 | :param args: Args for command. 1670 | :param check: Whether to check return code. 1671 | :param no_color: True to output not contain any color. 1672 | :param options: More command options. 1673 | """ 1674 | options.update( 1675 | no_color=flag(no_color), 1676 | ) 1677 | cmd = ["workspace", subcmd] 1678 | retcode, stdout, stderr = self.run( 1679 | cmd, args=args, options=options, chdir=self.cwd, check=check 1680 | ) 1681 | return CommandResult(retcode, stdout, stderr) 1682 | 1683 | def workspace_new( 1684 | self, 1685 | name: str, 1686 | check: bool = False, 1687 | no_color: bool = True, 1688 | lock: bool = None, 1689 | lock_timeout: str = None, 1690 | state: str = None, 1691 | **options, 1692 | ): 1693 | """Refer to https://www.terraform.io/docs/commands/workspace/new 1694 | 1695 | Create a new Terraform workspace. 1696 | 1697 | :param name: Workspace name. 1698 | :param check: Whether to check return code. 1699 | :param no_color: True to output not contain any color. 1700 | :param lock: False to not hold a state lock during backend migration. 1701 | This is dangerous if others might concurrently run commands against the 1702 | same workspace. 1703 | :param lock_timeout: Duration to retry a state lock. 1704 | :param state: Copy an existing state file into the new workspace. 1705 | :param options: More command options. 1706 | """ 1707 | options.update( 1708 | lock=lock, 1709 | lock_timeout=lock_timeout, 1710 | ) 1711 | return self.workspace( 1712 | "new", args=[name], check=check, no_color=no_color, state=state, **options 1713 | ) 1714 | 1715 | def workspace_list( 1716 | self, 1717 | check: bool = False, 1718 | no_color: bool = True, 1719 | **options, 1720 | ): 1721 | """Refer to https://www.terraform.io/docs/commands/workspace/list 1722 | 1723 | List Terraform workspaces. 1724 | 1725 | :param check: Whether to check return code. 1726 | :param no_color: True to output not contain any color. 1727 | :param options: More command options. 1728 | """ 1729 | return self.workspace("list", check=check, no_color=no_color, **options) 1730 | 1731 | def workspace_show( 1732 | self, 1733 | check: bool = False, 1734 | no_color: bool = True, 1735 | **options, 1736 | ): 1737 | """Refer to https://www.terraform.io/docs/commands/workspace/show 1738 | 1739 | Show the name of the current workspace. 1740 | 1741 | :param check: Whether to check return code. 1742 | :param no_color: True to output not contain any color. 1743 | :param options: More command options. 1744 | """ 1745 | return self.workspace("show", check=check, no_color=no_color, **options) 1746 | 1747 | def workspace_select( 1748 | self, 1749 | name: str, 1750 | check: bool = False, 1751 | no_color: bool = True, 1752 | **options, 1753 | ): 1754 | """Refer to https://www.terraform.io/docs/commands/workspace/select 1755 | 1756 | Select a different Terraform workspace. 1757 | 1758 | :param name: Workspace name. 1759 | :param check: Whether to check return code. 1760 | :param no_color: True to output not contain any color. 1761 | :param options: More command options. 1762 | """ 1763 | return self.workspace( 1764 | "select", args=[name], check=check, no_color=no_color, **options 1765 | ) 1766 | 1767 | def workspace_delete( 1768 | self, 1769 | name: str, 1770 | check: bool = False, 1771 | no_color: bool = True, 1772 | force: bool = None, 1773 | lock: bool = None, 1774 | lock_timeout: str = None, 1775 | **options, 1776 | ): 1777 | """Refer to https://www.terraform.io/docs/commands/workspace/delete 1778 | 1779 | Delete a Terraform workspace. 1780 | 1781 | :param name: Workspace name. 1782 | :param check: Whether to check return code. 1783 | :param no_color: True to remove even a non-empty workspace. 1784 | :param force: True to output not contain any color. 1785 | :param lock: False to not hold a state lock during backend migration. 1786 | This is dangerous if others might concurrently run commands against the 1787 | same workspace. 1788 | :param lock_timeout: Duration to retry a state lock. 1789 | :param options: More command options. 1790 | """ 1791 | options.update( 1792 | force=flag(force), 1793 | lock=lock, 1794 | lock_timeout=lock_timeout, 1795 | ) 1796 | return self.workspace( 1797 | "delete", args=[name], check=check, no_color=no_color, **options 1798 | ) 1799 | -------------------------------------------------------------------------------- /libterraform/common.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import List, Union 4 | 5 | # =================================================================== 6 | # OS constants 7 | # =================================================================== 8 | 9 | WINDOWS = os.name == "nt" 10 | 11 | # =================================================================== 12 | # Type 13 | # =================================================================== 14 | 15 | CmdType = Union[str, List] 16 | 17 | 18 | # =================================================================== 19 | # utils 20 | # =================================================================== 21 | 22 | 23 | def json_loads(string, split=False): 24 | if split: 25 | value = [] 26 | for line in string.split("\n"): 27 | if not line: 28 | continue 29 | value.append(json.loads(line)) 30 | else: 31 | value = json.loads(string) 32 | return value 33 | -------------------------------------------------------------------------------- /libterraform/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ctypes import * 3 | 4 | from libterraform import _free, _lib_tf 5 | from libterraform.exceptions import LibTerraformError 6 | 7 | 8 | class LoadConfigDirResult(Structure): 9 | _fields_ = [("r0", c_void_p), ("r1", c_void_p), ("r2", c_void_p)] 10 | 11 | 12 | _load_config_dir = _lib_tf.ConfigLoadConfigDir 13 | _load_config_dir.argtypes = [c_char_p] 14 | _load_config_dir.restype = LoadConfigDirResult 15 | 16 | 17 | class TerraformConfig: 18 | @staticmethod 19 | def load_config_dir(path: str) -> (dict, dict): 20 | """ 21 | load_config_dir reads the .tf and .tf.json files in the given directory 22 | as config files and then combines these files into a single Module. 23 | 24 | .tf files are parsed using the HCL native syntax while .tf.json files are 25 | parsed using the HCL JSON syntax. 26 | """ 27 | ret = _load_config_dir(path.encode("utf-8")) 28 | r_mod = cast(ret.r0, c_char_p).value 29 | _free(ret.r0) 30 | r_diags = cast(ret.r1, c_char_p).value 31 | _free(ret.r1) 32 | err = cast(ret.r2, c_char_p).value 33 | _free(ret.r2) 34 | 35 | if err: 36 | raise LibTerraformError(err) 37 | if r_mod is None: 38 | msg = f"The given directory {path!r} does not exist at all or could not be opened for some reason." 39 | raise LibTerraformError(msg) 40 | 41 | mod = json.loads(r_mod) 42 | diags = json.loads(r_diags) 43 | 44 | return mod, diags 45 | -------------------------------------------------------------------------------- /libterraform/exceptions.py: -------------------------------------------------------------------------------- 1 | class LibTerraformError(Exception): 2 | pass 3 | 4 | 5 | class TerraformCommandError(LibTerraformError): 6 | """Raised when TerraformCommand.run() is called with check=True and the process 7 | returns a non-zero exit status. 8 | 9 | Attributes: 10 | retcode, cmd, stdout, stderr 11 | """ 12 | 13 | def __init__(self, retcode, cmd, stdout=None, stderr=None): 14 | self.retcode = retcode 15 | self.cmd = cmd 16 | self.stdout = stdout 17 | self.stderr = stderr 18 | 19 | def __str__(self): 20 | return f"Command {self.cmd!r} returned non-zero exit status {self.retcode}." 21 | 22 | 23 | class TerraformFdReadError(LibTerraformError): 24 | """Raised when TerraformCommand.run() is called and cannot read stdout/stderr.""" 25 | 26 | def __init__(self, fd): 27 | self.fd = fd 28 | 29 | def __str__(self): 30 | return f"Read from fd {self.fd} error." 31 | -------------------------------------------------------------------------------- /plugin_patch.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | func CleanupAndRemoveClients() { 9 | // Set the killed to true so that we don't get unexpected panics 10 | atomic.StoreUint32(&Killed, 1) 11 | 12 | // Kill all the managed clients in parallel and use a WaitGroup 13 | // to wait for them all to finish up. 14 | var wg sync.WaitGroup 15 | managedClientsLock.Lock() 16 | for _, client := range managedClients { 17 | wg.Add(1) 18 | 19 | go func(client *Client) { 20 | client.Kill() 21 | wg.Done() 22 | }(client) 23 | } 24 | managedClients = managedClients[:0] 25 | managedClientsLock.Unlock() 26 | 27 | wg.Wait() 28 | } 29 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "colorama" 5 | version = "0.4.6" 6 | description = "Cross-platform colored terminal text." 7 | optional = false 8 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 9 | files = [ 10 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 11 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 12 | ] 13 | 14 | [[package]] 15 | name = "exceptiongroup" 16 | version = "1.2.2" 17 | description = "Backport of PEP 654 (exception groups)" 18 | optional = false 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 22 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 23 | ] 24 | 25 | [package.extras] 26 | test = ["pytest (>=6)"] 27 | 28 | [[package]] 29 | name = "importlib-metadata" 30 | version = "6.7.0" 31 | description = "Read metadata from Python packages" 32 | optional = false 33 | python-versions = ">=3.7" 34 | files = [ 35 | {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, 36 | {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, 37 | ] 38 | 39 | [package.dependencies] 40 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 41 | zipp = ">=0.5" 42 | 43 | [package.extras] 44 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 45 | perf = ["ipython"] 46 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 47 | 48 | [[package]] 49 | name = "iniconfig" 50 | version = "2.0.0" 51 | description = "brain-dead simple config-ini parsing" 52 | optional = false 53 | python-versions = ">=3.7" 54 | files = [ 55 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 56 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 57 | ] 58 | 59 | [[package]] 60 | name = "packaging" 61 | version = "24.0" 62 | description = "Core utilities for Python packages" 63 | optional = false 64 | python-versions = ">=3.7" 65 | files = [ 66 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 67 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 68 | ] 69 | 70 | [[package]] 71 | name = "pluggy" 72 | version = "1.2.0" 73 | description = "plugin and hook calling mechanisms for python" 74 | optional = false 75 | python-versions = ">=3.7" 76 | files = [ 77 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 78 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 79 | ] 80 | 81 | [package.dependencies] 82 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 83 | 84 | [package.extras] 85 | dev = ["pre-commit", "tox"] 86 | testing = ["pytest", "pytest-benchmark"] 87 | 88 | [[package]] 89 | name = "pytest" 90 | version = "7.4.4" 91 | description = "pytest: simple powerful testing with Python" 92 | optional = false 93 | python-versions = ">=3.7" 94 | files = [ 95 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 96 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 97 | ] 98 | 99 | [package.dependencies] 100 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 101 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 102 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 103 | iniconfig = "*" 104 | packaging = "*" 105 | pluggy = ">=0.12,<2.0" 106 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 107 | 108 | [package.extras] 109 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 110 | 111 | [[package]] 112 | name = "ruff" 113 | version = "0.5.2" 114 | description = "An extremely fast Python linter and code formatter, written in Rust." 115 | optional = false 116 | python-versions = ">=3.7" 117 | files = [ 118 | {file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"}, 119 | {file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"}, 120 | {file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"}, 121 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"}, 122 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"}, 123 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"}, 124 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"}, 125 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"}, 126 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"}, 127 | {file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"}, 128 | {file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"}, 129 | {file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"}, 130 | {file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"}, 131 | {file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"}, 132 | {file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"}, 133 | {file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"}, 134 | {file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"}, 135 | {file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"}, 136 | ] 137 | 138 | [[package]] 139 | name = "tomli" 140 | version = "2.0.1" 141 | description = "A lil' TOML parser" 142 | optional = false 143 | python-versions = ">=3.7" 144 | files = [ 145 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 146 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 147 | ] 148 | 149 | [[package]] 150 | name = "typing-extensions" 151 | version = "4.7.1" 152 | description = "Backported and Experimental Type Hints for Python 3.7+" 153 | optional = false 154 | python-versions = ">=3.7" 155 | files = [ 156 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 157 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 158 | ] 159 | 160 | [[package]] 161 | name = "zipp" 162 | version = "3.15.0" 163 | description = "Backport of pathlib-compatible object wrapper for zip files" 164 | optional = false 165 | python-versions = ">=3.7" 166 | files = [ 167 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 168 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 169 | ] 170 | 171 | [package.extras] 172 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 173 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 174 | 175 | [metadata] 176 | lock-version = "2.0" 177 | python-versions = "^3.7" 178 | content-hash = "e3963242c1511ff294121377c8372ff5e3042f3596c92367ee93526e83482f7a" 179 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libterraform" 3 | version = "0.8.0" 4 | description = "Python binding for Terraform." 5 | authors = ["Prodesire "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/Prodesire/py-libterraform" 9 | repository = "https://github.com/Prodesire/py-libterraform" 10 | keywords = ["libterraform", "terraform"] 11 | classifiers = [ 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.7", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Topic :: Software Development :: Libraries", 23 | "Operating System :: MacOS :: MacOS X", 24 | "Operating System :: POSIX", 25 | "Operating System :: POSIX :: BSD", 26 | "Operating System :: POSIX :: Linux", 27 | "Operating System :: Microsoft :: Windows" 28 | ] 29 | packages = [ 30 | { include = "libterraform/*" }, 31 | ] 32 | include = ["libterraform/libterraform.so", "libterraform/libterraform.dll"] 33 | 34 | [tool.poetry.dependencies] 35 | python = "^3.7" 36 | 37 | [tool.poetry.build] 38 | generate-setup-file = false 39 | script = "build.py" 40 | 41 | [tool.poetry.group.dev.dependencies] 42 | pytest = "^7.0.1" 43 | ruff = "^0.5.2" 44 | 45 | [build-system] 46 | requires = ["poetry-core>=1.0.0"] 47 | build-backend = "poetry.core.masonry.api" 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prodesire/py-libterraform/c1a2e79af90245902b013b7f48ad5f4f04b5a23f/tests/__init__.py -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prodesire/py-libterraform/c1a2e79af90245902b013b7f48ad5f4f04b5a23f/tests/cli/__init__.py -------------------------------------------------------------------------------- /tests/cli/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from libterraform import TerraformCommand 6 | from tests.consts import TF_SLEEP_DIR 7 | 8 | 9 | @pytest.fixture(scope="package") 10 | def cli(): 11 | cwd = TF_SLEEP_DIR 12 | tf = os.path.join(cwd, ".terraform") 13 | 14 | cli = TerraformCommand(cwd) 15 | if not os.path.exists(tf): 16 | cli.init() 17 | return cli 18 | -------------------------------------------------------------------------------- /tests/cli/test_apply.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from libterraform import TerraformCommand 4 | 5 | 6 | class TestTerraformCommandApply: 7 | def test_apply(self, cli: TerraformCommand): 8 | r = cli.apply() 9 | assert r.retcode == 0, r.error 10 | assert isinstance(r.value, list) 11 | 12 | def test_plan_and_apply(self, cli: TerraformCommand): 13 | tfstate_path = "terraform.tfstate" 14 | if os.path.exists(tfstate_path): 15 | os.remove(tfstate_path) 16 | 17 | tfplan_path = "sleep.tfplan" 18 | cli.plan(out=tfplan_path) 19 | r = cli.apply(tfplan_path) 20 | assert r.retcode == 0, r.error 21 | assert isinstance(r.value, list) 22 | -------------------------------------------------------------------------------- /tests/cli/test_destroy.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandApply: 5 | def test_destroy(self, cli: TerraformCommand): 6 | cli.apply() 7 | r = cli.destroy() 8 | assert r.retcode == 0, r.error 9 | -------------------------------------------------------------------------------- /tests/cli/test_fmt.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from libterraform import TerraformCommand 4 | from tests.consts import TF_SLEEP2_DIR, TF_SLEEP_DIR 5 | 6 | 7 | class TestTerraformCommandFmt: 8 | def test_fmt(self, cli: TerraformCommand): 9 | r = cli.fmt(list=False, write=False, diff=False, recursive=True) 10 | assert r.retcode == 0, r.error 11 | assert r.value 12 | 13 | def test_fmt_dir(self): 14 | cli = TerraformCommand() 15 | r = cli.fmt(TF_SLEEP_DIR, list=False, write=False, diff=False, recursive=True) 16 | assert r.retcode == 0, r.error 17 | assert r.value 18 | 19 | def test_fmt_dirs(self): 20 | cli = TerraformCommand() 21 | r = cli.fmt( 22 | [TF_SLEEP_DIR, TF_SLEEP2_DIR], 23 | list=False, 24 | write=False, 25 | diff=False, 26 | recursive=True, 27 | ) 28 | assert r.retcode == 0, r.error 29 | assert r.value 30 | -------------------------------------------------------------------------------- /tests/cli/test_force_unlock.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandForceUnlock: 5 | def test_force_unlock_invalid(self, cli: TerraformCommand): 6 | r = cli.force_unlock("invalid") 7 | assert r.retcode == 1 8 | assert "Failed to unlock state" in r.error 9 | -------------------------------------------------------------------------------- /tests/cli/test_get.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandGet: 5 | def test_get(self, cli: TerraformCommand): 6 | r = cli.get() 7 | assert r.retcode == 0, r.error 8 | -------------------------------------------------------------------------------- /tests/cli/test_graph.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandGraph: 5 | def test_graph(self, cli: TerraformCommand): 6 | r = cli.graph(draw_cycles=True) 7 | assert r.retcode == 0, r.error 8 | assert "digraph" in r.value 9 | 10 | def test_graph_by_plan(self, cli: TerraformCommand): 11 | tfplan_path = "sleep.tfplan" 12 | cli.plan(out=tfplan_path) 13 | r = cli.graph(plan=tfplan_path, draw_cycles=True) 14 | assert r.retcode == 0, r.error 15 | assert "digraph" in r.value 16 | -------------------------------------------------------------------------------- /tests/cli/test_import.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandImport: 5 | def test_import(self, cli: TerraformCommand): 6 | cli.destroy() 7 | try: 8 | r = cli.import_resource("time_sleep.wait1", "1s,") 9 | assert r.retcode == 0, r.error 10 | assert "Import successful!" in r.value 11 | assert "Import does not generate resource configuration" not in r.value 12 | finally: 13 | cli.destroy() 14 | -------------------------------------------------------------------------------- /tests/cli/test_init.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | from tests.consts import TF_SLEEP_DIR 3 | 4 | 5 | class TestTerraformCommandInit: 6 | def test_init(self): 7 | r = TerraformCommand(TF_SLEEP_DIR).init() 8 | assert r.retcode == 0, r.error 9 | -------------------------------------------------------------------------------- /tests/cli/test_output.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandOutput: 5 | def test_output(self, cli: TerraformCommand): 6 | cli.apply() 7 | r = cli.output() 8 | assert r.retcode == 0, r.error 9 | assert "wait1_id" in r.value 10 | assert "wait2_id" in r.value 11 | 12 | def test_output_with_name(self, cli: TerraformCommand): 13 | cli.apply() 14 | r = cli.output("wait1_id") 15 | assert r.retcode == 0, r.error 16 | assert isinstance(r.value, str) 17 | -------------------------------------------------------------------------------- /tests/cli/test_plan.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandPlan: 5 | def test_plan(self, cli: TerraformCommand): 6 | r = cli.plan() 7 | assert r.retcode == 0, r.error 8 | assert isinstance(r.value, list) 9 | 10 | def test_plan_with_vars(self, cli: TerraformCommand): 11 | r = cli.plan(vars={"time1": "1s", "time2": "2s"}) 12 | assert r.retcode == 0, r.error 13 | assert isinstance(r.value, list) 14 | -------------------------------------------------------------------------------- /tests/cli/test_providers.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from libterraform import TerraformCommand 4 | 5 | 6 | class TestTerraformCommandProviders: 7 | def test_providers(self, cli: TerraformCommand): 8 | r = cli.providers() 9 | assert r.retcode == 0, r.error 10 | 11 | def test_providers_lock(self, cli: TerraformCommand): 12 | cli.apply() 13 | r = cli.providers_lock( 14 | fs_mirror=os.path.join(cli.cwd, ".terraform", "providers"), 15 | enable_plugin_cache=True, 16 | ) 17 | assert r.retcode == 0, r.error 18 | 19 | def test_providers_schema(self, cli: TerraformCommand): 20 | r = cli.providers_schema() 21 | assert r.retcode == 0, r.error 22 | assert isinstance(r.value, dict) 23 | -------------------------------------------------------------------------------- /tests/cli/test_refresh.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import shutil 3 | 4 | from libterraform import TerraformCommand 5 | 6 | 7 | class TestTerraformCommandRefresh: 8 | def test_refresh(self, cli: TerraformCommand): 9 | r = cli.refresh() 10 | assert r.retcode == 0, r.error 11 | assert r.value 12 | 13 | def test_refresh_with_target(self, cli: TerraformCommand): 14 | r = cli.refresh(target="time_sleep.wait1") 15 | assert r.retcode == 0, r.error 16 | assert r.value 17 | 18 | def test_refresh_with_parallelism(self, cli: TerraformCommand): 19 | r = cli.refresh(parallelism=2) 20 | assert r.retcode == 0, r.error 21 | assert r.value 22 | -------------------------------------------------------------------------------- /tests/cli/test_run.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from libterraform import TerraformCommand 4 | from libterraform.exceptions import TerraformCommandError 5 | 6 | 7 | class TestTerraformCommandRun: 8 | def test_run_version(self): 9 | retcode, stdout, stderr = TerraformCommand.run("version") 10 | assert retcode == 0 11 | assert "Terraform" in stdout 12 | 13 | def test_run_invalid(self): 14 | retcode, stdout, stderr = TerraformCommand.run("invalid") 15 | assert retcode == 1 16 | assert 'Terraform has no command named "invalid"' in stderr 17 | 18 | with pytest.raises(TerraformCommandError): 19 | TerraformCommand.run("invalid", check=True) 20 | -------------------------------------------------------------------------------- /tests/cli/test_show.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandShow: 5 | def test_show(self, cli: TerraformCommand): 6 | r = cli.show() 7 | assert r.retcode == 0, r.error 8 | assert "format_version" in r.value 9 | 10 | def test_plan_and_show(self, cli: TerraformCommand): 11 | plan_path = "sleep.tfplan" 12 | cli.plan(out=plan_path) 13 | r = cli.show(plan_path) 14 | for key in ( 15 | "format_version", 16 | "terraform_version", 17 | "variables", 18 | "planned_values", 19 | "resource_changes", 20 | "configuration", 21 | ): 22 | assert key in r.value 23 | -------------------------------------------------------------------------------- /tests/cli/test_state.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import shutil 3 | 4 | from libterraform import TerraformCommand 5 | 6 | 7 | class TestTerraformCommandState: 8 | def test_state_list(self, cli: TerraformCommand): 9 | cli.apply() 10 | r = cli.state_list() 11 | assert r.retcode == 0, r.error 12 | assert r.value 13 | 14 | r = cli.state_list("time_sleep.wait1", "time_sleep.wait2") 15 | assert r.retcode == 0, r.error 16 | assert r.value 17 | 18 | def test_state_list_with_ids(self, cli: TerraformCommand): 19 | cli.apply() 20 | r = cli.output() 21 | id1 = r.value["wait1_id"]["value"] 22 | id2 = r.value["wait2_id"]["value"] 23 | r = cli.state_list(ids=[id1, id2]) 24 | assert r.retcode == 0, r.error 25 | assert r.value 26 | 27 | def test_state_list_with_state(self, cli: TerraformCommand): 28 | cli.apply() 29 | r = cli.state_list(state="terraform.tfstate") 30 | assert r.retcode == 0, r.error 31 | assert r.value 32 | 33 | def test_state_mv(self, cli: TerraformCommand): 34 | cli.apply() 35 | r = cli.state_mv("time_sleep.wait1", "time_sleep.wait1_new", dry_run=True) 36 | assert r.retcode == 0, r.error 37 | assert r.value 38 | 39 | def test_state_pull(self, cli: TerraformCommand): 40 | cli.apply() 41 | r = cli.state_pull() 42 | assert r.retcode == 0, r.error 43 | assert isinstance(r.value, dict) 44 | 45 | def test_state_push(self, cli: TerraformCommand): 46 | cli.apply() 47 | r = cli.state_push("terraform.tfstate") 48 | assert r.retcode == 0, r.error 49 | 50 | def test_state_replace_provider(self, cli: TerraformCommand): 51 | cli.apply() 52 | r = cli.state_replace_provider("hashicorp/time", "hashicorp/time") 53 | assert r.retcode == 0, r.error 54 | assert r.value 55 | 56 | def test_state_rm(self, cli: TerraformCommand): 57 | cli.apply() 58 | r = cli.state_rm("time_sleep.wait1", "time_sleep.wait2", dry_run=True) 59 | assert r.retcode == 0, r.error 60 | assert r.value 61 | 62 | def test_state_show(self, cli: TerraformCommand): 63 | cli.apply() 64 | r = cli.state_rm("time_sleep.wait1") 65 | assert r.retcode == 0, r.error 66 | assert r.value 67 | -------------------------------------------------------------------------------- /tests/cli/test_taint.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandTaint: 5 | def test_taint(self, cli: TerraformCommand): 6 | cli.apply() 7 | r = cli.taint("time_sleep.wait1") 8 | assert r.retcode == 0, r.error 9 | assert "time_sleep.wait1" in r.value 10 | 11 | def test_taint_allow_missing(self, cli: TerraformCommand): 12 | r = cli.taint("time_sleep.invalid", allow_missing=True) 13 | assert r.retcode == 0, r.error 14 | assert "No such resource instance" in r.value 15 | -------------------------------------------------------------------------------- /tests/cli/test_test.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from libterraform import TerraformCommand 4 | from tests.consts import TF_SLEEP2_DIR 5 | 6 | 7 | class TestTerraformCommandTest: 8 | def test_test(self, cli: TerraformCommand): 9 | r = cli.test(json=False) 10 | assert r.retcode == 0, r.error 11 | assert "Success! 0 passed, 0 failed." in r.value 12 | assert not r.error 13 | 14 | def test_test_run(self): 15 | cwd = TF_SLEEP2_DIR 16 | tf = os.path.join(cwd, ".terraform") 17 | 18 | cli = TerraformCommand(cwd) 19 | if not os.path.exists(tf): 20 | cli.init() 21 | r = cli.test() 22 | assert r.retcode == 0, r.error 23 | assert r.value[-1]["test_summary"]["status"] == "pass" 24 | 25 | def test_test_assertion_error(self): 26 | cwd = TF_SLEEP2_DIR 27 | tf = os.path.join(cwd, ".terraform") 28 | 29 | cli = TerraformCommand(cwd) 30 | if not os.path.exists(tf): 31 | cli.init() 32 | r = cli.test(vars={"sleep2_time1": "2s"}) 33 | assert r.retcode == 1 34 | assert r.value[-1]["test_summary"]["status"] == "fail" 35 | -------------------------------------------------------------------------------- /tests/cli/test_untaint.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandUnTaint: 5 | def test_untaint(self, cli: TerraformCommand): 6 | cli.apply() 7 | addr = "time_sleep.wait1" 8 | cli.taint(addr) 9 | r = cli.taint(addr) 10 | assert r.retcode == 0, r.error 11 | assert "time_sleep.wait1" in r.value 12 | 13 | def test_untaint_allow_missing(self, cli: TerraformCommand): 14 | r = cli.untaint("time_sleep.invalid", allow_missing=True) 15 | assert r.retcode == 0, r.error 16 | assert "No such resource instance" in r.value 17 | -------------------------------------------------------------------------------- /tests/cli/test_validate.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandValidate: 5 | def test_validate(self, cli: TerraformCommand): 6 | r = cli.validate() 7 | assert r.retcode == 0, r.error 8 | assert r.value == { 9 | "format_version": "1.0", 10 | "valid": True, 11 | "error_count": 0, 12 | "warning_count": 0, 13 | "diagnostics": [], 14 | } 15 | -------------------------------------------------------------------------------- /tests/cli/test_version.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandVersion: 5 | def test_version(self, cli: TerraformCommand): 6 | r = cli.version() 7 | assert r.json is True 8 | for key in ( 9 | "terraform_version", 10 | "platform", 11 | "provider_selections", 12 | "terraform_outdated", 13 | ): 14 | assert key in r.value 15 | 16 | def test_version_raw(self, cli: TerraformCommand): 17 | r = cli.version(json=False) 18 | assert r.json is False 19 | assert "Terraform" in r.value 20 | -------------------------------------------------------------------------------- /tests/cli/test_workspace.py: -------------------------------------------------------------------------------- 1 | from libterraform import TerraformCommand 2 | 3 | 4 | class TestTerraformCommandWorkSpace: 5 | def test_all(self, cli: TerraformCommand): 6 | default_name = "default" 7 | name = "test" 8 | r = cli.workspace_new(name) 9 | assert r.retcode == 0, r.error 10 | assert "Created and switched to workspace" in r.value 11 | 12 | r = cli.workspace_show() 13 | assert r.retcode == 0, r.error 14 | assert name in r.value 15 | 16 | r = cli.workspace_list() 17 | assert r.retcode == 0, r.error 18 | assert name in r.value 19 | assert default_name in r.value 20 | 21 | r = cli.workspace_select(default_name) 22 | assert r.retcode == 0, r.error 23 | 24 | r = cli.workspace_delete(name) 25 | assert r.retcode == 0, r.error 26 | -------------------------------------------------------------------------------- /tests/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prodesire/py-libterraform/c1a2e79af90245902b013b7f48ad5f4f04b5a23f/tests/config/__init__.py -------------------------------------------------------------------------------- /tests/config/test_load_config_dir.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from libterraform import TerraformConfig 4 | from libterraform.exceptions import LibTerraformError 5 | from tests.consts import TF_SLEEP_DIR 6 | 7 | 8 | class TestTerraformConfig: 9 | def test_load_config_dir(self): 10 | mod, diags = TerraformConfig.load_config_dir(TF_SLEEP_DIR) 11 | assert "time_sleep.wait1" in mod["ManagedResources"] 12 | assert "time_sleep.wait2" in mod["ManagedResources"] 13 | 14 | def test_load_config_dir_no_exits(self): 15 | with pytest.raises(LibTerraformError): 16 | TerraformConfig.load_config_dir("not-exits") 17 | -------------------------------------------------------------------------------- /tests/consts.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | ROOT = os.path.dirname(__file__) 4 | TF_DIR = os.path.join(ROOT, "tf") 5 | TF_SLEEP_DIR = os.path.join(TF_DIR, "sleep") 6 | TF_SLEEP2_DIR = os.path.join(TF_DIR, "sleep2") 7 | -------------------------------------------------------------------------------- /tests/tf/sleep/main.tf: -------------------------------------------------------------------------------- 1 | variable "time1" { 2 | type = string 3 | default = "1s" 4 | } 5 | 6 | variable "time2" { 7 | type = string 8 | default = "1s" 9 | } 10 | 11 | resource "time_sleep" "wait1" { 12 | create_duration = var.time1 13 | } 14 | 15 | resource "time_sleep" "wait2" { 16 | create_duration = var.time2 17 | } 18 | 19 | output "wait1_id" { 20 | value = time_sleep.wait1.id 21 | } 22 | 23 | output "wait2_id" { 24 | value = time_sleep.wait2.id 25 | } 26 | -------------------------------------------------------------------------------- /tests/tf/sleep2/main.tf: -------------------------------------------------------------------------------- 1 | variable "sleep2_time1" { 2 | type = string 3 | default = "1s" 4 | } 5 | 6 | variable "sleep2_time2" { 7 | type = string 8 | default = "1s" 9 | } 10 | 11 | resource "time_sleep" "sleep2_wait1" { 12 | create_duration = var.sleep2_time1 13 | } 14 | 15 | resource "time_sleep" "sleep2_wait2" { 16 | create_duration = var.sleep2_time2 17 | } 18 | 19 | output "sleep2_wait1_id" { 20 | value = time_sleep.sleep2_wait1.id 21 | } 22 | 23 | output "sleep2_wait2_id" { 24 | value = time_sleep.sleep2_wait2.id 25 | } 26 | -------------------------------------------------------------------------------- /tests/tf/sleep2/valid_sleep.tftest.hcl: -------------------------------------------------------------------------------- 1 | variables { 2 | sleep2_time2 = "2s" 3 | } 4 | 5 | run "valid_sleep_duration" { 6 | 7 | assert { 8 | condition = time_sleep.sleep2_wait1.create_duration == "1s" 9 | error_message = "libterraform test success!" 10 | } 11 | 12 | assert { 13 | condition = time_sleep.sleep2_wait2.create_duration == "2s" 14 | error_message = "Duration did not match expected" 15 | } 16 | 17 | } 18 | --------------------------------------------------------------------------------