├── .config └── ansible-lint.yml ├── .github ├── patchback.yml └── workflows │ ├── changelog.yaml │ ├── galaxy-import.yaml │ ├── integration-tests.yaml │ ├── linters.yaml │ ├── release-manual.yml │ ├── release-tag.yml │ ├── sanity-tests.yaml │ └── unit-tests.yaml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── changelogs ├── changelog.yaml ├── config.yaml └── fragments │ ├── .keep │ ├── 20240620-inventory-terraform_state-fix-issue-with-terraform-show.yaml │ └── 20240628-inventory-terraform_state-custom-providers.yaml ├── docs ├── cloud.terraform.plan_stash_module.rst ├── cloud.terraform.terraform_module.rst ├── cloud.terraform.terraform_output_module.rst ├── cloud.terraform.terraform_provider_inventory.rst ├── cloud.terraform.terraform_state_inventory.rst ├── cloud.terraform.tf_output_lookup.rst └── docsite │ ├── extra-docs.yml │ ├── links.yml │ └── rst │ └── guide_aap.rst ├── galaxy.yml ├── meta └── runtime.yml ├── plugins ├── action │ └── plan_stash.py ├── inventory │ ├── terraform_provider.py │ └── terraform_state.py ├── lookup │ └── tf_output.py ├── module_utils │ ├── errors.py │ ├── models.py │ ├── plan_stash_args.py │ ├── terraform_commands.py │ ├── types.py │ └── utils.py ├── modules │ ├── plan_stash.py │ ├── terraform.py │ └── terraform_output.py └── plugin_utils │ ├── base.py │ └── common.py ├── pyproject.toml ├── requirements.txt ├── roles ├── git_plan │ ├── README.md │ ├── meta │ │ ├── argument_specs.yml │ │ └── main.yml │ └── tasks │ │ └── main.yml └── inventory_from_outputs │ ├── README.md │ ├── meta │ ├── argument_specs.yml │ └── main.yml │ └── tasks │ └── main.yml ├── run_mypy.sh ├── test-requirements.txt ├── tests ├── config.yml ├── integration │ ├── requirements.txt │ ├── requirements.yml │ └── targets │ │ ├── action_groups │ │ ├── files │ │ │ └── nothing.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── aws │ │ ├── aliases │ │ ├── files │ │ │ └── cloud.tf │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ │ ├── aws_sqs_queue │ │ ├── aliases │ │ ├── files │ │ │ └── cloud.tf │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ │ ├── awscc │ │ ├── aliases │ │ ├── files │ │ │ └── cloud.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── azure │ │ ├── aliases │ │ ├── files │ │ │ └── cloud.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── complex_variables │ │ ├── files │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── gcp │ │ ├── aliases │ │ ├── files │ │ │ └── cloud.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── inventory_terraform_state_aws │ │ ├── aliases │ │ ├── runme.sh │ │ ├── setup.yml │ │ ├── tasks │ │ │ └── generate_tf.yml │ │ ├── teardown.yml │ │ ├── templates │ │ │ ├── aws_credentials.sh.j2 │ │ │ ├── backend.hcl.j2 │ │ │ ├── inventory.yml.j2 │ │ │ ├── inventory_search_child_module.yml.j2 │ │ │ ├── inventory_with_backend_files.yml.j2 │ │ │ ├── inventory_with_compose.yml.j2 │ │ │ ├── inventory_with_constructed.yml.j2 │ │ │ ├── inventory_with_hostname.yml.j2 │ │ │ ├── main.child.tf.j2 │ │ │ └── main.tf.j2 │ │ ├── test.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── inventory_terraform_state_azurerm │ │ ├── aliases │ │ ├── runme.sh │ │ ├── setup.yml │ │ ├── teardown.yml │ │ ├── templates │ │ │ ├── azure_credentials.sh.j2 │ │ │ ├── inventory.yml.j2 │ │ │ └── main.tf.j2 │ │ ├── test.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── inventory_terraform_state_google │ │ ├── aliases │ │ ├── runme.sh │ │ ├── setup.yml │ │ ├── teardown.yml │ │ ├── templates │ │ │ ├── gcp_credentials.sh.j2 │ │ │ ├── inventory.yml.j2 │ │ │ └── main.tf.j2 │ │ ├── test.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── list_vars_passthrough │ │ ├── files │ │ │ └── main.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── local │ │ ├── files │ │ │ └── write_file.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── output_lookup │ │ ├── outputs.tf │ │ ├── runme.sh │ │ └── test_outputs.yml │ │ ├── output_lookup_cwd │ │ ├── outputs.tf │ │ ├── runme.sh │ │ └── test_outputs.yml │ │ ├── output_lookup_datadir │ │ ├── outputs.tf │ │ ├── runme.sh │ │ └── test_outputs.yml │ │ ├── output_lookup_workspace │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── outputs.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── plan_stash │ │ ├── files │ │ │ └── main.tf │ │ └── tasks │ │ │ ├── main.yml │ │ │ ├── run.yml │ │ │ └── validate_args.yml │ │ ├── provider_upgrade │ │ ├── tasks │ │ │ ├── main.yml │ │ │ └── test_provider_upgrade.yml │ │ ├── templates │ │ │ └── main.tf.j2 │ │ └── vars │ │ │ └── main.yml │ │ ├── state_planned │ │ ├── files │ │ │ └── write_file.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── terraform_diff │ │ ├── files │ │ │ ├── secret.tfvars │ │ │ ├── write_file.tf │ │ │ └── write_file_updated.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── terraform_output │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ ├── outputs.tf │ │ │ └── outputs_workspace.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── terraform_plan_file │ │ ├── aliases │ │ ├── files │ │ │ └── main.tf │ │ └── tasks │ │ │ └── main.yml │ │ ├── terraform_provider │ │ ├── ansible_provider.tf │ │ ├── inventory1.yml │ │ ├── inventory2.yml │ │ ├── inventory3.yml │ │ ├── inventory4.yml │ │ ├── modules │ │ │ ├── example │ │ │ │ ├── main.tf │ │ │ │ ├── provider.tf │ │ │ │ └── variables.tf │ │ │ └── nested_module │ │ │ │ ├── main.tf │ │ │ │ ├── provider.tf │ │ │ │ └── variables.tf │ │ ├── runme.sh │ │ └── test.yml │ │ ├── test_git_plan │ │ ├── files │ │ │ └── write_file.tf │ │ └── tasks │ │ │ └── main.yml │ │ └── test_inventory_from_outputs │ │ ├── files │ │ └── create_inventory.tf │ │ └── tasks │ │ └── main.yml └── unit │ └── plugins │ ├── inventory │ ├── test_terraform_provider.py │ └── test_terraform_state.py │ ├── lookup │ └── test_tf_output.py │ ├── module_utils │ ├── test_models.py │ └── test_terraform_commands.py │ ├── modules │ ├── cloud │ │ └── misc │ │ │ └── test_terraform.py │ ├── test_terraform.py │ ├── test_terraform_output.py │ └── utils.py │ └── plugin_utils │ └── test_common.py └── tox.ini /.config/ansible-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | profile: production 3 | 4 | exclude_paths: 5 | - .ansible/ 6 | - changelogs/changelog.yaml 7 | - tests/integration 8 | -------------------------------------------------------------------------------- /.github/patchback.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backport_branch_prefix: patchback/backports/ 3 | backport_label_prefix: backport- 4 | target_branch_prefix: stable- 5 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Changelog 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | pull_request: 10 | types: 11 | - opened 12 | - reopened 13 | - labeled 14 | - unlabeled 15 | - synchronize 16 | branches: 17 | - main 18 | - stable-* 19 | 20 | jobs: 21 | changelog: 22 | uses: ansible-network/github_actions/.github/workflows/changelog.yml@main 23 | -------------------------------------------------------------------------------- /.github/workflows/galaxy-import.yaml: -------------------------------------------------------------------------------- 1 | name: Galaxy import 2 | 3 | concurrency: 4 | group: ${{ github.head_ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - main 11 | - stable-* 12 | 13 | jobs: 14 | galaxy-import: 15 | uses: ansible-network/github_actions/.github/workflows/galaxy_importer.yml@main 16 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Integration tests 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - reopened 8 | - labeled 9 | - unlabeled 10 | - synchronize 11 | branches: 12 | - main 13 | - stable-* 14 | 15 | jobs: 16 | safe-to-test: 17 | if: ${{ github.event.label.name == 'safe to test' }} || ${{ github.event.action != 'labeled' }} 18 | uses: ansible-network/github_actions/.github/workflows/safe-to-test.yml@main 19 | 20 | integration-tests: 21 | needs: 22 | - safe-to-test 23 | runs-on: ubuntu-latest 24 | env: 25 | source: "./source" 26 | python_version: "3.12" 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | ansible-version: 31 | - milestone 32 | - devel 33 | name: "integration-tests-${{ matrix.ansible-version }}" 34 | steps: 35 | - name: Checkout collection 36 | uses: actions/checkout@v4 37 | with: 38 | path: ${{ env.source }} 39 | ref: ${{ github.event.pull_request.head.sha }} 40 | 41 | - name: Set up Python ${{ env.python_version }} 42 | uses: actions/setup-python@v4 43 | with: 44 | python-version: ${{ env.python_version }} 45 | 46 | - name: Install ansible-core (${{ matrix.ansible-version }}) 47 | run: >- 48 | python3 -m pip install 49 | https://github.com/ansible/ansible/archive/${{ matrix.ansible-version }}.tar.gz 50 | shell: bash 51 | 52 | - name: Pre install collections dependencies first so the collection install does not 53 | run: >- 54 | ansible-galaxy collection install 55 | --pre "-r${{ env.source }}/tests/integration/requirements.yml" 56 | -p /home/runner/collections/ 57 | shell: bash 58 | 59 | - name: Build and install collection 60 | id: install-collection 61 | uses: ansible-network/github_actions/.github/actions/build_install_collection@main 62 | with: 63 | install_python_dependencies: true 64 | source_path: ${{ env.source }} 65 | 66 | - name: Install Terraform binary 67 | uses: hashicorp/setup-terraform@v3 68 | with: 69 | terraform_wrapper: false 70 | terraform_version: "1.6.3" 71 | 72 | - name: Create AWS/sts session credentials 73 | uses: ansible-network/github_actions/.github/actions/ansible_aws_test_provider@main 74 | with: 75 | collection_path: ${{ steps.install-collection.outputs.collection_path }} 76 | ansible_core_ci_key: ${{ secrets.ANSIBLE_CORE_CI_KEY }} 77 | 78 | - name: Create AzureRM session credentials 79 | uses: ansible-network/github_actions/.github/actions/ansible_azure_test_provider@main 80 | with: 81 | collection_path: ${{ steps.install-collection.outputs.collection_path }} 82 | ansible_core_ci_key: ${{ secrets.ANSIBLE_CORE_CI_KEY }} 83 | 84 | # we use raw git to create a repository in the tests 85 | # this fails if the committer doesn't have a name and an email set 86 | - name: Set up git 87 | run: | 88 | git config --global user.email gha@localhost 89 | git config --global user.name "Github Actions" 90 | shell: bash 91 | 92 | - name: Run integration tests 93 | uses: ansible-network/github_actions/.github/actions/ansible_test_integration@main 94 | with: 95 | collection_path: ${{ steps.install-collection.outputs.collection_path }} 96 | python_version: "${{ env.python_version }}" 97 | ansible_version: "${{ matrix.ansible-version }}" 98 | -------------------------------------------------------------------------------- /.github/workflows/linters.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linters 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | pull_request: 10 | branches: 11 | - main 12 | - stable-* 13 | 14 | jobs: 15 | ansible-lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: run-ansible-lint 21 | uses: ansible/ansible-lint@v25.1.2 22 | 23 | tox-linters: 24 | uses: ansible-network/github_actions/.github/workflows/tox-linters.yml@main 25 | -------------------------------------------------------------------------------- /.github/workflows/release-manual.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate GitHub Release (manual trigger) 3 | concurrency: 4 | group: release-${{ github.head_ref }} 5 | cancel-in-progress: true 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | release: 10 | required: true 11 | description: "Existing tag to generate release for (example: 3.0.0)" 12 | type: string 13 | 14 | jobs: 15 | generate-release-log: 16 | permissions: 17 | contents: read 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Generate Release Log 21 | uses: ansible-collections/amazon.aws/.github/actions/ansible_release_log@main 22 | with: 23 | release: ${{ inputs.release }} 24 | 25 | perform-release: 26 | permissions: 27 | contents: write 28 | runs-on: ubuntu-latest 29 | needs: 30 | - generate-release-log 31 | steps: 32 | - name: Generate Release 33 | uses: ansible-collections/amazon.aws/.github/actions/ansible_release_tag@main 34 | with: 35 | release: ${{ inputs.release }} 36 | collection-name: cloud.terraform 37 | -------------------------------------------------------------------------------- /.github/workflows/release-tag.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate GitHub Release 3 | concurrency: 4 | group: release-${{ github.head_ref }} 5 | cancel-in-progress: true 6 | on: 7 | push: 8 | tags: 9 | - "*" 10 | 11 | jobs: 12 | generate-release-log: 13 | permissions: 14 | contents: read 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Generate Release Log 18 | uses: ansible-collections/amazon.aws/.github/actions/ansible_release_log@main 19 | with: 20 | release: ${{ github.ref_name }} 21 | 22 | perform-release: 23 | permissions: 24 | contents: write 25 | runs-on: ubuntu-latest 26 | needs: 27 | - generate-release-log 28 | steps: 29 | - name: Generate Release 30 | uses: ansible-collections/amazon.aws/.github/actions/ansible_release_tag@main 31 | with: 32 | release: ${{ github.ref_name }} 33 | collection-name: cloud.terraform 34 | -------------------------------------------------------------------------------- /.github/workflows/sanity-tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sanity tests 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | pull_request: 10 | branches: 11 | - main 12 | - stable-* 13 | 14 | jobs: 15 | sanity-tests: 16 | uses: ansible-network/github_actions/.github/workflows/sanity.yml@main 17 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - main 11 | - stable-* 12 | 13 | jobs: 14 | unit-tests: 15 | runs-on: ubuntu-latest 16 | name: Unit tests (Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ansible: 21 | - stable-2.14 22 | - stable-2.15 23 | - stable-2.16 24 | - milestone 25 | - devel 26 | python: 27 | - '3.9' 28 | - '3.10' 29 | - '3.11' 30 | - '3.12' 31 | exclude: 32 | - ansible: stable-2.14 33 | python: '3.12' 34 | - ansible: stable-2.15 35 | python: '3.12' 36 | - ansible: stable-2.16 37 | python: '3.9' 38 | - ansible: milestone 39 | python: '3.9' 40 | - ansible: devel 41 | python: '3.9' 42 | continue-on-error: ${{ matrix.ansible == 'devel' }} 43 | steps: 44 | - name: Perform unit testing 45 | uses: ansible-community/ansible-test-gh-action@release/v1 46 | with: 47 | ansible-core-version: ${{ matrix.ansible }} 48 | testing-type: units 49 | target-python-version: ${{ matrix.python }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | terraform 2 | .idea/ 3 | output/ 4 | .collection_root/* 5 | 6 | tests/integration/inventory 7 | tests/integration/cloud-config-* 8 | 9 | **/.terraform/* 10 | *.tfstate 11 | *.tfstate.* 12 | .terraform.lock.hcl 13 | 14 | /tests/output/ 15 | /changelogs/.plugin-cache.yaml 16 | 17 | # Byte-compiled / optimized / DLL files 18 | __pycache__/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | pip-wheel-metadata/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | *.py,cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # IPython 97 | profile_default/ 98 | ipython_config.py 99 | 100 | # pyenv 101 | .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | -------------------------------------------------------------------------------- /changelogs/config.yaml: -------------------------------------------------------------------------------- 1 | changelog_filename_template: ../CHANGELOG.rst 2 | changelog_filename_version_depth: 0 3 | changes_file: changelog.yaml 4 | changes_format: combined 5 | keep_fragments: false 6 | mention_ancestor: true 7 | new_plugins_after_name: removed_features 8 | notesdir: fragments 9 | prelude_section_name: release_summary 10 | prelude_section_title: Release Summary 11 | sections: 12 | - - breaking_changes 13 | - Breaking Changes / Porting Guide 14 | - - major_changes 15 | - Major Changes 16 | - - minor_changes 17 | - Minor Changes 18 | - - deprecated_features 19 | - Deprecated Features 20 | - - removed_features 21 | - Removed Features (previously deprecated) 22 | - - security_fixes 23 | - Security Fixes 24 | - - bugfixes 25 | - Bugfixes 26 | - - known_issues 27 | - Known Issues 28 | title: The cloud.terraform collection 29 | trivial_section_name: trivial 30 | -------------------------------------------------------------------------------- /changelogs/fragments/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-collections/cloud.terraform/aa17e71f67d10fd1a1e32a31f10a68e2ecc6c067/changelogs/fragments/.keep -------------------------------------------------------------------------------- /changelogs/fragments/20240620-inventory-terraform_state-fix-issue-with-terraform-show.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bugfixes: 3 | - inventory/terraform_state - use ``terraform pull`` instead of ``terraform show`` to 4 | parse raw state file to avoid provider versioning constraints 5 | (https://github.com/ansible-collections/cloud.terraform/issues/151). 6 | -------------------------------------------------------------------------------- /changelogs/fragments/20240628-inventory-terraform_state-custom-providers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | minor_changes: 3 | - inventory/terraform_state - Support for custom Terraform providers (https://github.com/ansible-collections/cloud.terraform/pull/146). 4 | -------------------------------------------------------------------------------- /docs/docsite/extra-docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sections: 3 | - title: Scenario Guides 4 | toctree: 5 | - guide_aap 6 | -------------------------------------------------------------------------------- /docs/docsite/links.yml: -------------------------------------------------------------------------------- 1 | --- 2 | edit_on_github: 3 | repository: ansible-collections/cloud.terraform 4 | branch: main 5 | path_prefix: '' 6 | 7 | extra_links: 8 | - description: Report an issue 9 | url: https://github.com/ansible-collections/cloud.terraform/issues/new/choose 10 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html 2 | 3 | namespace: cloud 4 | name: terraform 5 | version: 4.0.0-dev0 6 | readme: README.md 7 | authors: 8 | - Ansible (https://github.com/ansible) 9 | description: Terraform collection for Ansible. 10 | license_file: LICENSE 11 | tags: 12 | - terraform 13 | - cloud 14 | - infrastructure 15 | - cluster 16 | repository: https://github.com/ansible-collections/cloud.terraform 17 | homepage: https://github.com/ansible-collections/cloud.terraform 18 | issues: https://github.com/ansible-collections/cloud.terraform/issues 19 | build_ignore: 20 | # https://docs.ansible.com/ansible/devel/dev_guide/developing_collections_distributing.html#ignoring-files-and-folders 21 | - .gitignore 22 | - changelogs/.plugin-cache.yaml 23 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | requires_ansible: ">=2.15.0" 3 | action_groups: 4 | terraform: 5 | - terraform 6 | - terraform_output 7 | -------------------------------------------------------------------------------- /plugins/action/plan_stash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Contributors to the Ansible project 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from ansible.plugins.action import ActionBase 8 | from ansible.utils.vars import isidentifier 9 | from ansible_collections.cloud.terraform.plugins.module_utils.plan_stash_args import PLAN_STASH_ARG_SPEC 10 | 11 | 12 | class ActionModule(ActionBase): # type: ignore # mypy ignore 13 | def run(self, tmp=None, task_vars=None): # type: ignore # mypy ignore 14 | if task_vars is None: 15 | task_vars = dict() 16 | 17 | result = super(ActionModule, self).run(tmp, task_vars) 18 | del tmp # tmp no longer has any effect 19 | 20 | validation_result, new_module_args = self.validate_argument_spec(PLAN_STASH_ARG_SPEC) 21 | 22 | # Validate that 'var_name' is a valid variable name 23 | var_name = new_module_args.get("var_name") 24 | binary_data = new_module_args.get("binary_data") 25 | if var_name: 26 | if not isidentifier(var_name): 27 | result["failed"] = True 28 | result["msg"] = ( 29 | "The variable name '%s' is not valid. Variables must start with a letter or underscore character, and contain only " 30 | "letters, numbers and underscores." % var_name 31 | ) 32 | return result 33 | 34 | state = new_module_args.get("state") 35 | if state == "load": 36 | if var_name is not None and binary_data is not None: 37 | result["failed"] = True 38 | result["msg"] = "You cannot specify both 'var_name' and 'binary_data' to load the terraform plan file." 39 | return result 40 | 41 | if binary_data is None: 42 | var_name = new_module_args.get("var_name") or "terraform_plan" 43 | try: 44 | value = task_vars[var_name] 45 | except KeyError: 46 | try: 47 | value = task_vars["hostvars"][task_vars["inventory_hostname"]][var_name] 48 | except KeyError: 49 | result["failed"] = True 50 | result["msg"] = "No variable found with this name: %s" % var_name 51 | return result 52 | 53 | new_module_args.pop("var_name") 54 | new_module_args["binary_data"] = value 55 | elif state == "stash": 56 | var_name = new_module_args.get("var_name") or "terraform_plan" 57 | new_module_args.update({"var_name": var_name}) 58 | 59 | # Execute the plan_stash module. 60 | module_return = self._execute_module( 61 | module_name=self._task.action, 62 | module_args=new_module_args, 63 | task_vars=task_vars, 64 | ) 65 | 66 | result.update(module_return) 67 | return result 68 | -------------------------------------------------------------------------------- /plugins/lookup/tf_output.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | # language=yaml 4 | DOCUMENTATION = """ 5 | name: tf_output 6 | author: Polona Mihalič (@PolonaM) 7 | version_added: 1.0.0 8 | short_description: Reads state file outputs. 9 | description: 10 | - This lookup returns all outputs or selected output in state file. 11 | options: 12 | _terms: 13 | description: 14 | - Name(s) of the output(s) to return. 15 | - If value is not set, all outputs will be returned in a dictionary. 16 | type: str 17 | project_path: 18 | description: 19 | - The path to the root of the Terraform directory with the terraform.tfstate file. 20 | - If I(state_file) and I(project_path) are not specified, the C(terraform.tfstate) file in the 21 | current working directory will be used. 22 | - The C(TF_DATA_DIR) environment variable is respected. 23 | type: path 24 | state_file: 25 | description: 26 | - The path to an existing Terraform state file whose outputs will be listed. 27 | - If I(state_file) and I(project_path) are not specified, the C(terraform.tfstate) file in the 28 | current working directory will be used. 29 | - The C(TF_DATA_DIR) environment variable is respected. 30 | type: path 31 | binary_path: 32 | description: 33 | - The path of a terraform binary to use. 34 | type: path 35 | workspace: 36 | description: 37 | - The terraform workspace to work with. 38 | type: str 39 | version_added: 1.2.0 40 | """ 41 | 42 | # language=yaml 43 | EXAMPLES = """ 44 | - name: get selected output from terraform.tfstate 45 | ansible.builtin.debug: 46 | msg: "{{ lookup('cloud.terraform.tf_output', 'my_output1', project_path='path/to/project/dir/') }}" 47 | 48 | - name: get all outputs from custom state file 49 | ansible.builtin.debug: 50 | msg: "{{ lookup('cloud.terraform.tf_output', state_file='path/to/custom/state/file') }}" 51 | 52 | - name: get all outputs from terraform.tfstate in cwd 53 | ansible.builtin.debug: 54 | msg: "{{ lookup('cloud.terraform.tf_output') }}" 55 | 56 | - name: get all outputs from terraform.tfstate in workspace 'dev' 57 | ansible.builtin.debug: 58 | msg: "{{ lookup('cloud.terraform.tf_output', workspace='dev') }}" 59 | """ 60 | 61 | # language=yaml 62 | RETURN = """ 63 | _outputs: 64 | description: 65 | - A list of dict that contains all outputs. 66 | returned: when _terms is not specified 67 | type: list 68 | elements: dict 69 | _value: 70 | description: 71 | - A list of selected output's value. 72 | returned: when name(s) of the output(s) is specified 73 | type: list 74 | elements: str 75 | """ 76 | 77 | 78 | import os 79 | import subprocess 80 | from typing import Dict, List, Optional, Tuple 81 | 82 | from ansible.module_utils.common import process 83 | from ansible.plugins.lookup import LookupBase 84 | from ansible_collections.cloud.terraform.plugins.module_utils.types import AnyJsonType 85 | from ansible_collections.cloud.terraform.plugins.module_utils.utils import get_outputs 86 | 87 | 88 | # no module available here, mock functionality to be consistent throughout the rest of the codebase 89 | def module_run_command( 90 | cmd: List[str], cwd: str, environ_update: Optional[Dict[str, str]] = None 91 | ) -> Tuple[int, str, str]: 92 | env = os.environ.copy() 93 | env.update(environ_update or {}) 94 | completed_process = subprocess.run(cmd, capture_output=True, check=False, cwd=cwd, env=env) 95 | return ( 96 | completed_process.returncode, 97 | completed_process.stdout.decode("utf-8"), 98 | completed_process.stderr.decode("utf-8"), 99 | ) 100 | 101 | 102 | class LookupModule(LookupBase): # type: ignore # cannot subclass without available type (implicitly Any) 103 | def run(self, terms: List[str], variables: Optional[Dict[str, str]] = None, **kwargs: str) -> List[AnyJsonType]: 104 | self.set_options(var_options=variables, direct=kwargs) 105 | project_path = self.get_option("project_path") 106 | state_file = self.get_option("state_file") 107 | bin_path = self.get_option("binary_path") 108 | workspace = self.get_option("workspace") 109 | 110 | if bin_path is not None: 111 | terraform_binary = bin_path 112 | else: 113 | terraform_binary = process.get_bin_path("terraform", required=True) 114 | 115 | output: List[AnyJsonType] = [] 116 | if not terms: 117 | output.append( 118 | get_outputs( 119 | module_run_command, 120 | terraform_binary, 121 | project_path, 122 | state_file, 123 | output_format="json", 124 | workspace=workspace, 125 | ) 126 | ) 127 | else: 128 | for term in terms: 129 | value = get_outputs( 130 | module_run_command, 131 | terraform_binary, 132 | project_path, 133 | state_file, 134 | name=term, 135 | output_format="json", 136 | workspace=workspace, 137 | ) 138 | output.append(value) 139 | return output 140 | -------------------------------------------------------------------------------- /plugins/module_utils/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, NoReturn 2 | 3 | from ansible.module_utils.basic import AnsibleModule 4 | 5 | 6 | class TerraformCollectionException(Exception): 7 | def __init__(self, message: str) -> None: 8 | self.message = message 9 | 10 | 11 | class TerraformWarning(TerraformCollectionException): 12 | """An exception that results in a non-fatal warning.""" 13 | 14 | 15 | class TerraformError(TerraformCollectionException): 16 | """An exception that results in a fatal error.""" 17 | 18 | def __init__(self, message: str, **kwargs: Any): 19 | super().__init__(message) 20 | if kwargs is None: 21 | self.kwargs: Dict[str, Any] = {} 22 | else: 23 | self.kwargs = kwargs 24 | 25 | def fail_json(self, module: AnsibleModule) -> NoReturn: 26 | module.fail_json(msg=self.message, **self.kwargs) 27 | # fail_json does not return, hinting to the type checker explicitly 28 | raise AssertionError("fail_json should have exited the process") 29 | -------------------------------------------------------------------------------- /plugins/module_utils/plan_stash_args.py: -------------------------------------------------------------------------------- 1 | PLAN_STASH_ARG_SPEC = { 2 | "path": {"required": True, "type": "path"}, 3 | "var_name": {}, 4 | "per_host": {"type": "bool", "default": False}, 5 | "state": {"choices": ["stash", "load"], "default": "stash"}, 6 | "binary_data": {"type": "raw"}, 7 | } 8 | -------------------------------------------------------------------------------- /plugins/module_utils/types.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict, List, Tuple, Union 2 | 3 | AnyJsonType = Union[Dict[str, "AnyJsonType"], List["AnyJsonType"], str, int, float, bool, None] 4 | TJsonObject = Dict[str, AnyJsonType] 5 | TJsonList = List[AnyJsonType] 6 | TJsonBareValue = Union[str, int, float, bool, None] 7 | 8 | AnsibleRunCommandType = Callable[..., Tuple[int, str, str]] 9 | -------------------------------------------------------------------------------- /plugins/module_utils/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | from typing import List, Optional, Union, cast 5 | 6 | from ansible.module_utils.compat.version import LooseVersion 7 | from ansible_collections.cloud.terraform.plugins.module_utils.errors import TerraformError, TerraformWarning 8 | from ansible_collections.cloud.terraform.plugins.module_utils.terraform_commands import TerraformCommands 9 | from ansible_collections.cloud.terraform.plugins.module_utils.types import ( 10 | AnsibleRunCommandType, 11 | TJsonBareValue, 12 | TJsonObject, 13 | ) 14 | 15 | 16 | def get_state_args(state_file: Optional[str]) -> List[str]: 17 | if state_file is not None: 18 | if not os.path.exists(state_file): 19 | raise TerraformError('Could not find state_file "{0}", check the path and try again.'.format(state_file)) 20 | return ["-state", state_file] 21 | return [] 22 | 23 | 24 | def get_outputs( 25 | run_command_fp: AnsibleRunCommandType, 26 | terraform_binary: str, 27 | project_path: Optional[str], 28 | state_file: Optional[str], 29 | output_format: str, 30 | name: Optional[str] = None, 31 | workspace: Optional[str] = None, 32 | ) -> Union[TJsonObject, TJsonBareValue]: 33 | outputs_command = [terraform_binary, "output", "-no-color", "-{0}".format(output_format)] 34 | outputs_command += get_state_args(state_file) + ([name] if name else []) 35 | if workspace: 36 | tf_env = {"TF_WORKSPACE": workspace} 37 | rc, outputs_text, outputs_err = run_command_fp(outputs_command, cwd=project_path, environ_update=tf_env) 38 | else: 39 | rc, outputs_text, outputs_err = run_command_fp(outputs_command, cwd=project_path) 40 | if rc == 1: 41 | message = ( 42 | "Could not get Terraform outputs. " 43 | "This usually means none have been defined.\ncommand: {0}\nstdout: {1}\nstderr: {2}".format( 44 | outputs_command, outputs_text, outputs_err 45 | ) 46 | ) 47 | raise TerraformWarning(message) 48 | elif rc != 0: 49 | message = "Failure when getting Terraform outputs. Exited {0}.\nstdout: {1}\nstderr: {2}".format( 50 | rc, outputs_text, outputs_err 51 | ) 52 | raise TerraformError(message, command=" ".join(outputs_command)) 53 | else: 54 | if output_format == "raw": 55 | return cast(TJsonObject, outputs_text) 56 | else: 57 | outputs = cast(TJsonObject, json.loads(outputs_text)) 58 | return outputs 59 | 60 | 61 | def validate_project_path(project_path: str) -> None: 62 | if project_path is None or "/" not in project_path: 63 | raise TerraformError("Path for Terraform project can not be None or ''.") 64 | 65 | if not os.path.isdir(project_path): 66 | raise TerraformError( 67 | "Path for Terraform project '{0}' doesn't exist on this host - check the path and try again please.".format( 68 | project_path 69 | ) 70 | ) 71 | 72 | 73 | def validate_bin_path(bin_path: str) -> None: 74 | if not shutil.which(bin_path): 75 | raise TerraformError( 76 | "Path for Terraform binary '{0}' doesn't exist on this host - check the path and try again please.".format( 77 | bin_path 78 | ) 79 | ) 80 | 81 | 82 | def preflight_validation( 83 | terraform: TerraformCommands, 84 | bin_path: str, 85 | project_path: str, 86 | version: LooseVersion, 87 | variables_args: List[str], 88 | ) -> None: 89 | validate_project_path(project_path) 90 | validate_bin_path(bin_path) 91 | terraform.validate(version, variables_args) 92 | -------------------------------------------------------------------------------- /plugins/modules/plan_stash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2024, Aubin Bikouo 4 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | 7 | DOCUMENTATION = r""" 8 | --- 9 | module: plan_stash 10 | version_added: 2.1.0 11 | short_description: Handle the base64 encoding or decoding of a terraform plan file 12 | description: 13 | - This module performs base64-encoding of a terraform plan file and saves it into playbook execution stats similar 14 | to M(ansible.builtin.set_stats) module. 15 | - The module also performs base64-decoding of a terraform plan file from a variable defined into ansible facts and writes them 16 | into a file specified by the user. 17 | author: 18 | - "Aubin Bikouo (@abikouo)" 19 | options: 20 | state: 21 | description: 22 | - "O(state=stash): base64-encodes the terraform plan file and saves it into ansible stats like using the M(ansible.builtin.set_stats) module." 23 | - "O(state=load): base64-decodes data from variable specified in O(var_name) and writes them into terraform plan file." 24 | choices: [stash, load] 25 | default: stash 26 | type: str 27 | path: 28 | description: 29 | - The path to the terraform plan file. 30 | type: path 31 | required: true 32 | var_name: 33 | description: 34 | - When O(state=stash), this parameter defines the variable name to be set into stats. 35 | - When O(state=load), this parameter defines the variable from ansible facts containing 36 | the base64-encoded data of the terraform plan file. 37 | - Variables must start with a letter or underscore character, and contain only letters, 38 | numbers and underscores. 39 | - The module will use V(terraform_plan) as default variable name if not specified. 40 | type: str 41 | binary_data: 42 | description: 43 | - When O(state=load), this parameter defines the base64-encoded data of the terraform plan file. 44 | - Mutually exclusive with V(var_name). 45 | - Ignored when O(state=stash). 46 | type: raw 47 | per_host: 48 | description: 49 | - Whether the stats are per host or for all hosts in the run. 50 | - Ignored when O(state=load). 51 | type: bool 52 | default: false 53 | notes: 54 | - For security reasons, this module should be used with I(no_log=true) and I(register) functionalities 55 | as the plan file can contain unencrypted secrets. 56 | """ 57 | 58 | EXAMPLES = r""" 59 | # Encode terraform plan file into default variable 'terraform_plan' 60 | - name: Encode a terraform plan file into terraform_plan variable 61 | cloud.terraform.plan_stash: 62 | path: /path/to/terraform_plan_file 63 | state: stash 64 | no_log: true 65 | 66 | # Encode terraform plan file into variable 'stashed_plan' 67 | - name: Encode a terraform plan file into terraform_plan variable 68 | cloud.terraform.plan_stash: 69 | path: /path/to/terraform_plan_file 70 | var_name: stashed_plan 71 | state: stash 72 | no_log: true 73 | 74 | # Load terraform plan file from variable 'stashed_plan' 75 | - name: Load a terraform plan file data from variable 'stashed_plan' into file 'tfplan' 76 | cloud.terraform.plan_stash: 77 | path: tfplan 78 | var_name: stashed_plan 79 | state: load 80 | no_log: true 81 | 82 | # Load terraform plan file from binary data 83 | - name: Load a terraform plan file data from binary data 84 | cloud.terraform.plan_stash: 85 | path: tfplan 86 | binary_data: "{{ terraform_binary_data }}" 87 | state: load 88 | no_log: true 89 | """ 90 | 91 | RETURN = r""" 92 | """ 93 | 94 | import base64 95 | 96 | from ansible.module_utils.basic import AnsibleModule 97 | from ansible_collections.cloud.terraform.plugins.module_utils.plan_stash_args import PLAN_STASH_ARG_SPEC 98 | 99 | 100 | def read_file_content(file_path: str, module: AnsibleModule, failed_on_error: bool = True) -> bytes: 101 | data = b"" 102 | try: 103 | with open(file_path, "rb") as f: 104 | data = f.read() 105 | except FileNotFoundError: 106 | if failed_on_error: 107 | module.fail_json(msg="The following file '{0}' does not exist.".format(file_path)) 108 | return data 109 | 110 | 111 | def main() -> None: 112 | module = AnsibleModule( 113 | argument_spec=PLAN_STASH_ARG_SPEC, 114 | supports_check_mode=True, 115 | ) 116 | 117 | terrafom_plan_file = module.params.get("path") 118 | var_name = module.params.get("var_name") 119 | per_host = module.params.get("per_host") 120 | state = module.params.get("state") 121 | 122 | result = {} 123 | if state == "stash": 124 | # Stash: base64-encode the terraform plan file and set stats 125 | data = read_file_content(terrafom_plan_file, module) 126 | # encode binary data 127 | try: 128 | encoded_data = base64.b64encode(data) 129 | except Exception as e: 130 | module.fail_json(msg="Cannot encode data from file {0} due to: {1}".format(terrafom_plan_file, e)) 131 | 132 | stats = {"data": {var_name: encoded_data}, "per_host": per_host} 133 | result = {"ansible_stats": stats, "changed": False} 134 | else: 135 | # Load: Decodes the data from the variable name and write into terraform plan file 136 | binary_data = module.params.get("binary_data") 137 | try: 138 | data = base64.b64decode(binary_data) 139 | except Exception as e: 140 | module.fail_json(msg="Failed to decode binary data due to: {0}".format(e)) 141 | 142 | current_content = read_file_content(terrafom_plan_file, module, failed_on_error=False) 143 | changed = False 144 | if current_content != data: 145 | changed = True 146 | if not module.check_mode: 147 | try: 148 | with open(terrafom_plan_file, "wb") as f: 149 | f.write(data) 150 | result.update({"msg": "data successfully decoded into file %s" % terrafom_plan_file}) 151 | except Exception as e: 152 | module.fail_json(msg="Failed to write data into file due to: {0}".format(e)) 153 | 154 | result.update({"changed": changed}) 155 | 156 | module.exit_json(**result) 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /plugins/modules/terraform_output.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017, Ryan Scott Brown 4 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | 7 | # language=yaml 8 | DOCUMENTATION = r""" 9 | --- 10 | module: terraform_output 11 | short_description: Returns Terraform module outputs. 12 | description: 13 | - Returns Terraform module outputs. 14 | options: 15 | project_path: 16 | description: 17 | - The path to the root of the Terraform directory with the terraform.tfstate file. 18 | - If I(state_file) and I(project_path) are not specified, the C(terraform.tfstate) file in the 19 | current working directory will be used. 20 | - The C(TF_DATA_DIR) environment variable is respected. 21 | type: path 22 | version_added: 1.0.0 23 | name: 24 | description: 25 | - Name of an individual output in the state file to list. 26 | type: str 27 | version_added: 1.0.0 28 | format: 29 | description: 30 | - A flag to specify the output format. Defaults to C(json). 31 | - I(name) must be provided when using C(raw) option. 32 | choices: [ json, raw ] 33 | default: json 34 | type: str 35 | version_added: 1.0.0 36 | binary_path: 37 | description: 38 | - The path of a terraform binary to use. 39 | type: path 40 | version_added: 1.0.0 41 | state_file: 42 | description: 43 | - The path to an existing Terraform state file whose outputs will be listed. 44 | - If I(state_file) and I(project_path) are not specified, the C(terraform.tfstate) file in the 45 | current working directory will be used. 46 | - The C(TF_DATA_DIR) environment variable is respected. 47 | type: path 48 | version_added: 1.0.0 49 | workspace: 50 | description: 51 | - The terraform workspace to work with. 52 | type: str 53 | version_added: 1.2.0 54 | requirements: [ "terraform" ] 55 | author: "Polona Mihalič (@PolonaM)" 56 | """ 57 | 58 | # language=yaml 59 | EXAMPLES = """ 60 | - name: List outputs from terraform.tfstate in project_dir 61 | cloud.terraform.terraform_output: 62 | project_path: project_dir 63 | 64 | - name: List outputs from selected state file in project_dir 65 | cloud.terraform.terraform_output: 66 | state_file: state_file 67 | 68 | - name: List outputs from terraform.tfstate in project_dir, use different Terraform version 69 | cloud.terraform.terraform_output: 70 | project_path: project_dir 71 | binary_path: terraform_binary 72 | 73 | - name: List value of an individual output from terraform.tfstate in project_dir 74 | cloud.terraform.terraform_output: 75 | project_path: project_dir 76 | name: individual_output 77 | 78 | - name: List value of an individual output in raw format 79 | cloud.terraform.terraform_output: 80 | project_path: project_dir 81 | name: individual_output 82 | format: raw 83 | 84 | - name: List outputs from workspace 'dev' in project_dir 85 | cloud.terraform.terraform_output: 86 | project_path: project_dir 87 | workspace: dev 88 | """ 89 | 90 | # language=yaml 91 | RETURN = """ 92 | outputs: 93 | type: dict 94 | description: A dictionary of all the TF outputs by their assigned name. Use C(.outputs.MyOutputName.value) to access the value. 95 | returned: when name is not specified 96 | sample: '{"bukkit_arn": {"sensitive": false, "type": "string", "value": "arn:aws:s3:::tf-test-bukkit"}' 97 | contains: 98 | sensitive: 99 | type: bool 100 | returned: always 101 | description: Whether Terraform has marked this value as sensitive 102 | type: 103 | type: str 104 | returned: always 105 | description: The type of the value (string, int, etc) 106 | value: 107 | type: str 108 | returned: always 109 | description: The value of the output as interpolated by Terraform 110 | value: 111 | type: str 112 | description: A single value requested by the module using the "name" parameter 113 | sample: "myvalue" 114 | returned: when name is specified 115 | """ 116 | 117 | 118 | from typing import Optional 119 | 120 | from ansible.module_utils.basic import AnsibleModule 121 | from ansible_collections.cloud.terraform.plugins.module_utils.errors import TerraformError, TerraformWarning 122 | from ansible_collections.cloud.terraform.plugins.module_utils.utils import get_outputs, validate_bin_path 123 | 124 | 125 | def main() -> None: 126 | module = AnsibleModule( 127 | argument_spec=dict( 128 | project_path=dict(type="path"), 129 | name=dict(type="str"), 130 | format=dict(type="str", choices=["json", "raw"], default="json"), 131 | binary_path=dict(type="path"), 132 | state_file=dict(type="path"), 133 | workspace=dict(type="str"), 134 | ), 135 | required_if=[("format", "raw", ("name",))], 136 | ) 137 | 138 | project_path: Optional[str] = module.params.get("project_path") 139 | bin_path: Optional[str] = module.params.get("binary_path") 140 | state_file: Optional[str] = module.params.get("state_file") 141 | name: Optional[str] = module.params.get("name") 142 | output_format: str = module.params.get("format") 143 | workspace: Optional[str] = module.params.get("workspace") 144 | 145 | if bin_path is not None: 146 | terraform_binary = bin_path 147 | else: 148 | terraform_binary = module.get_bin_path("terraform", required=True) 149 | validate_bin_path(terraform_binary) 150 | 151 | try: 152 | outputs = get_outputs( 153 | module.run_command, 154 | terraform_binary=terraform_binary, 155 | project_path=project_path, 156 | state_file=state_file, 157 | name=name, 158 | output_format=output_format, 159 | workspace=workspace, 160 | ) 161 | except TerraformWarning as e: 162 | module.warn(e.message) 163 | outputs = None 164 | except TerraformError as e: 165 | e.fail_json(module) 166 | 167 | if name: 168 | module.exit_json(value=outputs) 169 | else: 170 | module.exit_json(outputs=outputs) 171 | 172 | 173 | if __name__ == "__main__": 174 | main() 175 | -------------------------------------------------------------------------------- /plugins/plugin_utils/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (c) 2023 Red Hat Inc. 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from typing import Any 7 | 8 | from ansible.plugins.inventory import BaseInventoryPlugin 9 | from ansible.utils.display import Display 10 | 11 | display = Display() 12 | 13 | 14 | class TerraformInventoryPluginBase(BaseInventoryPlugin): # type: ignore # mypy ignore 15 | def warn(self, message: Any) -> None: 16 | display.warning(message) 17 | 18 | def debug(self, message: Any) -> None: 19 | display.debug(message) 20 | -------------------------------------------------------------------------------- /plugins/plugin_utils/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright: Ansible Project 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | import subprocess 7 | from typing import List, Tuple 8 | 9 | 10 | # no module available here, mock functionality to be consistent throughout the rest of the codebase 11 | def module_run_command(cmd: List[str], cwd: str, check_rc: bool) -> Tuple[int, str, str]: 12 | completed_process = subprocess.run(cmd, capture_output=True, check=check_rc, cwd=cwd) 13 | return ( 14 | completed_process.returncode, 15 | completed_process.stdout.decode("utf-8"), 16 | completed_process.stderr.decode("utf-8"), 17 | ) 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ["py39", "py310", "py311", "py312"] 4 | include = "\\.pyi?$" 5 | workers = 4 6 | 7 | [tool.isort] 8 | profile = "black" 9 | line_length = 120 10 | 11 | [tool.mypy] 12 | strict = true 13 | pretty = true 14 | show_error_codes = true 15 | show_error_context = true 16 | show_column_numbers = true 17 | warn_unused_configs = true 18 | color_output = true 19 | 20 | namespace_packages = true 21 | explicit_package_bases = true 22 | 23 | # ignores for dependencies without type information 24 | [[tool.mypy.overrides]] 25 | module = [ 26 | "ansible.*", 27 | ] 28 | ignore_missing_imports = true 29 | 30 | # this module parses JSON and would need casts and asserts all over the place 31 | # because parsing fails anyway, we ignore these two errors, as 32 | # we assume that the JSON structure Terraform returns is consistent 33 | [[tool.mypy.overrides]] 34 | module = [ 35 | "ansible_collections.cloud.terraform.plugins.module_utils.models", 36 | ] 37 | disable_error_code = ["arg-type", "union-attr"] 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml 2 | -------------------------------------------------------------------------------- /roles/git_plan/README.md: -------------------------------------------------------------------------------- 1 | git_plan 2 | ================== 3 | 4 | Clone a Git repository and apply a plan from it. 5 | 6 | Requirements 7 | ------------ 8 | 9 | - NA 10 | 11 | Role Variables 12 | -------------- 13 | 14 | * **project_path**: The path to the root of the Terraform directory with the .tfstate file. *Mutually exclusive with state_file*. 15 | * **state_file**: An absolute path to an existing Terraform state file. *Mutually exclusive with project_path*. 16 | * **mapping_variables**: Names that define the mapping between Terraform output variables and inventory host variables. Contains the following: 17 | - **host_list**: (Required) The Terraform variable that contains the list of hosts to be processed into the in-memory inventory. Other keys in the mapping_variables parameter refer to properties of the items of this list. 18 | - **name**: (Required) The Terraform variable that contains the name of the resulting inventory host. 19 | - **ip**: (Required) The Terraform variable that contains the IP or hostname of the resulting inventory host. Maps directly to ansible_host. 20 | - **user**: (Required) The Terraform variable that contains the username of the resulting inventory host. Maps directly to ansible_user. 21 | - **group**: (Required) The Terraform variable that contains the group the resulting host will be a member of. 22 | 23 | * **repo_url**: (Required) The URL of the repository to clone. 24 | * **repo_dir**: (Required) The directory to clone the Git repository into. 25 | * **version**: The ref of the repository to use. Defaults to the remote HEAD. 26 | * **plan_file**: (Required) The plan file to use. This must exist. 27 | * **git_options**: Options to configure ansible.builtin.git. Names correspond to module arguments. See ansible.builtin.git documentation for details. 28 | * **terraform_options**: Parameters for module cloud.terraform.terraform. See cloud.terraform.terraform documentation for details. 29 | 30 | Limitations 31 | ------------ 32 | 33 | - NA 34 | 35 | Dependencies 36 | ------------ 37 | 38 | - NA 39 | 40 | Example Playbook 41 | ---------------- 42 | 43 | - hosts: localhost 44 | roles: 45 | - role: cloud.terraform.git_plan 46 | project_path: 'my_project_directory' 47 | mapping_variables: 48 | host_lists: terraform_var_host_list 49 | name: terraform_var_name 50 | ip: terraform_var_ip 51 | user: terraform_var_user 52 | group: terraform_var_group 53 | 54 | License 55 | ------- 56 | 57 | GNU General Public License v3.0 or later 58 | 59 | See [LICENSE](https://github.com/ansible-collections/cloud.terraform/blob/main/LICENSE) to see the full text. 60 | -------------------------------------------------------------------------------- /roles/git_plan/meta/argument_specs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | argument_specs: 3 | main: 4 | short_description: Clone a Git repository and apply a plan from it. 5 | description: 6 | - Clone a Git repository and apply a plan from it. 7 | options: 8 | repo_url: 9 | description: The URL of the repository to clone. 10 | type: str 11 | required: true 12 | version_added: 1.0.0 13 | repo_dir: 14 | description: The directory to clone the Git repository into. 15 | type: str 16 | required: true 17 | version_added: 1.0.0 18 | version: 19 | description: The ref of the repository to use. Defaults to the remote HEAD. 20 | type: str 21 | required: false 22 | version_added: 1.0.0 23 | plan_file: 24 | description: The plan file to use. This must exist. 25 | type: str 26 | required: true 27 | version_added: 1.0.0 28 | 29 | git_options: 30 | description: | 31 | Options to configure ansible.builtin.git. 32 | Names correspond to module arguments. 33 | See ansible.builtin.git documentation for details. 34 | type: dict 35 | required: false 36 | version_added: 1.0.0 37 | options: 38 | accept_hostkey: 39 | type: bool 40 | required: false 41 | version_added: 1.0.0 42 | accept_newhostkey: 43 | type: bool 44 | required: false 45 | version_added: 1.0.0 46 | depth: 47 | type: int 48 | required: false 49 | version_added: 1.0.0 50 | executable: 51 | type: str 52 | required: false 53 | version_added: 1.0.0 54 | force: 55 | type: bool 56 | required: false 57 | version_added: 1.0.0 58 | gpg_whitelist: 59 | type: list 60 | elements: str 61 | required: false 62 | version_added: 1.0.0 63 | key_file: 64 | type: str 65 | required: false 66 | version_added: 1.0.0 67 | remote: 68 | type: str 69 | required: false 70 | version_added: 1.0.0 71 | separate_git_dir: 72 | type: str 73 | required: false 74 | version_added: 1.0.0 75 | ssh_opts: 76 | type: str 77 | required: false 78 | version_added: 1.0.0 79 | track_submodules: 80 | type: bool 81 | required: false 82 | version_added: 1.0.0 83 | verify_commit: 84 | type: bool 85 | required: false 86 | version_added: 1.0.0 87 | terraform_options: 88 | description: Options to configure terraform execution. 89 | options: 90 | state_file: 91 | description: An optional state file to use, overriding the default. 92 | type: str 93 | required: false 94 | version_added: 1.0.0 95 | force_init: 96 | type: bool 97 | required: false 98 | version_added: 1.0.0 99 | binary_path: 100 | type: str 101 | required: false 102 | version_added: 1.0.0 103 | plugin_paths: 104 | type: list 105 | elements: path 106 | required: false 107 | version_added: 1.0.0 108 | workspace: 109 | type: str 110 | required: false 111 | version_added: 1.0.0 112 | lock: 113 | type: bool 114 | required: false 115 | version_added: 1.0.0 116 | lock_timeout: 117 | type: int 118 | required: false 119 | version_added: 1.0.0 120 | parallelism: 121 | type: int 122 | required: false 123 | version_added: 1.0.0 124 | -------------------------------------------------------------------------------- /roles/git_plan/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Ansible cloud team. 4 | description: A role to clone a Git repository and apply a plan from it. 5 | license: GPL-3.0-or-later 6 | min_ansible_version: 2.15.0 7 | -------------------------------------------------------------------------------- /roles/git_plan/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fetch git repository 3 | ansible.builtin.git: 4 | repo: "{{ repo_url }}" 5 | dest: "{{ repo_dir }}" 6 | version: "{{ version | default(omit) }}" 7 | clone: true 8 | # optional config 9 | accept_hostkey: "{{ git_options.accept_hostkey | default(omit) }}" 10 | accept_newhostkey: "{{ git_options.accept_newhostkey | default(omit) }}" 11 | depth: "{{ git_options.depth | default(omit) }}" 12 | executable: "{{ git_options.executable | default(omit) }}" 13 | force: "{{ git_options.force | default(omit) }}" 14 | gpg_whitelist: "{{ git_options.gpg_whitelist | default(omit) }}" 15 | key_file: "{{ git_options.key_file | default(omit) }}" 16 | remote: "{{ git_options.remote | default(omit) }}" 17 | separate_git_dir: "{{ git_options.separate_git_dir | default(omit) }}" 18 | ssh_opts: "{{ git_options.ssh_opts | default(omit) }}" 19 | track_submodules: "{{ git_options.track_submodules | default(omit) }}" 20 | verify_commit: "{{ git_options.verify_commit | default(omit) }}" 21 | 22 | - name: Apply plan 23 | cloud.terraform.terraform: 24 | project_path: "{{ repo_dir }}" 25 | plan_file: "{{ plan_file }}" 26 | state: present # applying a plan doesn't have a switch for this 27 | # optional config 28 | state_file: "{{ terraform_options.state_file | default(omit) }}" 29 | force_init: "{{ terraform_options.force_init | default(omit) }}" 30 | binary_path: "{{ terraform_options.binary_path | default(omit) }}" 31 | plugin_paths: "{{ terraform_options.plugin_paths | default(omit) }}" 32 | workspace: "{{ terraform_options.workspace | default(omit) }}" 33 | lock: "{{ terraform_options.lock | default(omit) }}" 34 | lock_timeout: "{{ terraform_options.lock_timeout | default(omit) }}" 35 | parallelism: "{{ terraform_options.parallelism | default(omit) }}" 36 | -------------------------------------------------------------------------------- /roles/inventory_from_outputs/README.md: -------------------------------------------------------------------------------- 1 | inventory_from_outputs 2 | ================== 3 | 4 | A role to create an in-memory inventory from Terraform outputs. 5 | 6 | Requirements 7 | ------------ 8 | 9 | - NA 10 | 11 | Role Variables 12 | -------------- 13 | 14 | * **project_path**: The path to the root of the Terraform directory with the .tfstate file. If not set, the current working directory is used as a Terraform directory. 15 | * **state_file**: The path to an existing Terraform state file. If not set, terraform.tfstate in selected directory is used. 16 | * **mapping_variables**: Names that define the mapping between Terraform output variables and inventory host variables. Contains the following: 17 | - **host_list**: (Required) The Terraform variable that contains the list of hosts to be processed into the in-memory inventory. Other keys in the mapping_variables parameter refer to properties of the items of this list. 18 | - **name**: (Required) The Terraform variable that contains the name of the resulting inventory host. 19 | - **ip**: (Required) The Terraform variable that contains the IP or hostname of the resulting inventory host. Maps directly to ansible_host. 20 | - **user**: (Required) The Terraform variable that contains the username of the resulting inventory host. Maps directly to ansible_user. 21 | - **group**: (Required) The Terraform variable that contains the group the resulting host will be a member of. 22 | 23 | Limitations 24 | ------------ 25 | 26 | - NA 27 | 28 | Dependencies 29 | ------------ 30 | 31 | - NA 32 | 33 | Example Playbook 34 | ---------------- 35 | 36 | - hosts: localhost 37 | roles: 38 | - role: cloud.terraform.inventory_from_outputs 39 | project_path: 'my_project_directory' 40 | mapping_variables: 41 | host_list: terraform_var_host_list 42 | name: terraform_var_name 43 | ip: terraform_var_ip 44 | user: terraform_var_user 45 | group: terraform_var_group 46 | 47 | Example of inventory definition in .tf file 48 | ---------------- 49 | ``` 50 | output "terraform_var_host_list" { 51 | value = [ 52 | { "terraform_var_ip" : "my_ip1", "terraform_var_group" : "my_group1", "terraform_var_name" : "my_name1", "terraform_var_user" : "my_user" }, 53 | { "terraform_var_ip" : "my_ip2", "terraform_var_group" : "my_group2", "terraform_var_name" : "my_name2", "terraform_var_user" : "my_user" }, 54 | { "terraform_var_ip" : "my_ip3", "terraform_var_group" : "my_group1", "terraform_var_name" : "my_name3", "terraform_var_user" : "my_user" }, 55 | ] 56 | } 57 | ``` 58 | 59 | License 60 | ------- 61 | 62 | GNU General Public License v3.0 or later 63 | 64 | See [LICENSE](https://github.com/ansible-collections/cloud.terraform/blob/main/LICENSE) to see the full text. 65 | -------------------------------------------------------------------------------- /roles/inventory_from_outputs/meta/argument_specs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | argument_specs: 3 | main: 4 | short_description: Create an in-memory inventory from Terraform outputs. 5 | description: 6 | - Create an in-memory inventory from Terraform outputs. 7 | options: 8 | project_path: 9 | description: 10 | - The path to the root of the Terraform directory with the .tfstate file. 11 | - If I(state_file) and I(project_path) are not specified, the C(terraform.tfstate) file in the 12 | current working directory will be used. 13 | - The C(TF_DATA_DIR) environment variable is respected. 14 | type: path 15 | version_added: 1.0.0 16 | state_file: 17 | description: 18 | - The path to an existing Terraform state file. 19 | - If I(state_file) and I(project_path) are not specified, the C(terraform.tfstate) file in the 20 | current working directory will be used. 21 | - The C(TF_DATA_DIR) environment variable is respected. 22 | type: path 23 | version_added: 1.0.0 24 | mapping_variables: 25 | description: Names that define the mapping between Terraform output variables and inventory host variables. 26 | type: dict 27 | version_added: 1.0.0 28 | required: true 29 | options: 30 | host_list: 31 | description: 32 | - The Terraform variable that contains the list of hosts to be processed into the in-memory inventory. 33 | - Other keys in the mapping_variables parameter refer to properties of the items of this list. 34 | type: str 35 | required: true 36 | version_added: 1.0.0 37 | name: 38 | description: The Terraform variable that contains the name of the resulting inventory host. 39 | type: str 40 | required: true 41 | version_added: 1.0.0 42 | ip: 43 | description: 44 | - The Terraform variable that contains the IP or hostname of the resulting inventory host. 45 | - Maps directly to ansible_host. 46 | type: str 47 | required: true 48 | version_added: 1.0.0 49 | user: 50 | description: 51 | - The Terraform variable that contains the username of the resulting inventory host. 52 | - Maps directly to ansible_user. 53 | type: str 54 | required: true 55 | version_added: 1.0.0 56 | group: 57 | description: The Terraform variable that contains the group the resulting host will be a member of. 58 | type: str 59 | required: true 60 | version_added: 1.0.0 61 | -------------------------------------------------------------------------------- /roles/inventory_from_outputs/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Ansible cloud team. 4 | description: A role to create an in-memory inventory from Terraform outputs. 5 | license: GPL-3.0-or-later 6 | min_ansible_version: 2.15.0 7 | -------------------------------------------------------------------------------- /roles/inventory_from_outputs/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Read outputs from project path 3 | cloud.terraform.terraform_output: 4 | project_path: "{{ project_path }}" 5 | state_file: "{{ state_file }}" 6 | register: terraform_output 7 | 8 | - name: Add hosts from terraform_output to the group defined in terraform_output 9 | ansible.builtin.add_host: 10 | name: "{{ item[mapping_variables.name] }}" 11 | groups: "{{ item[mapping_variables.group] }}" 12 | ansible_host: "{{ item[mapping_variables.ip] }}" 13 | ansible_user: "{{ item[mapping_variables.user] }}" 14 | loop: "{{ terraform_output.outputs[mapping_variables.host_list].value }}" 15 | -------------------------------------------------------------------------------- /run_mypy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 5 | 6 | rm -rf "${SCRIPT_DIR}/.collection_root" 7 | mkdir -p "${SCRIPT_DIR}/.collection_root/ansible_collections/cloud" 8 | ln -s "${SCRIPT_DIR}" "${SCRIPT_DIR}/.collection_root/ansible_collections/cloud/terraform" 9 | cd "${SCRIPT_DIR}/.collection_root/ansible_collections/cloud/terraform/" 10 | export MYPYPATH="${SCRIPT_DIR}/.collection_root" 11 | mypy -p ansible_collections.cloud.terraform.plugins 12 | rm -rf "${SCRIPT_DIR}/.collection_root" -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | awscli 3 | google-auth 4 | google-cloud 5 | pytest 6 | pytest-forked 7 | pytest-mock 8 | pytest-xdist 9 | types-PyYAML 10 | packaging 11 | requests[security] 12 | xmltodict 13 | msgraph-sdk==1.6.0 14 | azure-cli-core==2.64.0 15 | azure-common==1.1.28 16 | azure-identity==1.19.0 17 | azure-mgmt-authorization==4.0.0 18 | azure-mgmt-apimanagement==4.0.1 19 | azure-mgmt-batch==17.3.0 20 | azure-mgmt-compute==33.0.0 21 | azure-mgmt-cdn==13.1.1 22 | azure-mgmt-containerinstance==10.1.0 23 | azure-mgmt-core==1.4.0 24 | azure-mgmt-containerregistry==10.3.0 25 | azure-containerregistry==1.2.0 26 | azure-mgmt-containerservice==32.1.0 27 | azure-mgmt-datafactory==9.0.0 28 | azure-mgmt-dns==8.1.0 29 | azure-mgmt-marketplaceordering==1.2.0b2 30 | azure-mgmt-monitor==6.0.2 31 | azure-mgmt-managedservices==7.0.0b2 32 | azure-mgmt-managementgroups==1.1.0b2 33 | azure-mgmt-network==28.0.0 34 | azure-mgmt-nspkg==3.0.2 35 | azure-mgmt-privatedns==1.1.0 36 | azure-mgmt-redis==14.4.0 37 | azure-mgmt-resource==23.2.0 38 | azure-mgmt-rdbms==10.2.0b17 39 | azure-mgmt-postgresqlflexibleservers==1.1.0 40 | azure-mgmt-search==9.2.0b2 41 | azure-mgmt-servicebus==8.2.1 42 | azure-mgmt-sql==4.0.0b19 43 | azure-mgmt-storage==21.2.1 44 | azure-mgmt-trafficmanager==1.1.0 45 | azure-mgmt-web==7.3.1 46 | azure-nspkg==3.0.2 47 | azure-storage-blob==12.23.0b1 48 | azure-core==1.31.0 49 | azure-keyvault==4.2.0 50 | azure-mgmt-keyvault==10.3.1 51 | azure-mgmt-cosmosdb==10.0.0b3 52 | azure-mgmt-hdinsight==9.1.0b1 53 | azure-mgmt-devtestlabs==10.0.0b2 54 | azure-mgmt-loganalytics==13.0.0b7 55 | azure-mgmt-automation==1.1.0b4 56 | azure-mgmt-iothub==3.0.0 57 | azure-iot-hub==2.6.1;platform_machine=="x86_64" 58 | azure-mgmt-recoveryservices==3.0.0 59 | azure-mgmt-recoveryservicesbackup==9.1.0 60 | azure-mgmt-notificationhubs==8.1.0b1 61 | azure-mgmt-eventhub==11.1.0 62 | azure-mgmt-resourcehealth==1.0.0b6 63 | oras 64 | netaddr 65 | -------------------------------------------------------------------------------- /tests/config.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | python_requires: ">=3.9" 3 | -------------------------------------------------------------------------------- /tests/integration/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | botocore 3 | packaging 4 | requests[security] 5 | xmltodict 6 | msgraph-sdk==1.0.0 7 | azure-cli-core==2.34.0 8 | azure-common==1.1.11 9 | azure-identity==1.14.0 10 | azure-mgmt-authorization==2.0.0 11 | azure-mgmt-apimanagement==3.0.0 12 | azure-mgmt-batch==16.2.0 13 | azure-mgmt-cdn==11.0.0 14 | azure-mgmt-compute==26.1.0 15 | azure-mgmt-containerinstance==9.0.0 16 | azure-mgmt-core==1.3.0 17 | azure-mgmt-containerregistry==9.1.0 18 | azure-containerregistry==1.1.0 19 | azure-mgmt-containerservice==20.0.0 20 | azure-mgmt-datalake-store==1.0.0 21 | azure-mgmt-datafactory==2.0.0 22 | azure-mgmt-dns==8.0.0 23 | azure-mgmt-marketplaceordering==1.1.0 24 | azure-mgmt-monitor==3.0.0 25 | azure-mgmt-managedservices==6.0.0 26 | azure-mgmt-managementgroups==1.0.0 27 | azure-mgmt-network==19.1.0 28 | azure-mgmt-nspkg==2.0.0 29 | azure-mgmt-privatedns==1.0.0 30 | azure-mgmt-redis==13.0.0 31 | azure-mgmt-resource==21.1.0 32 | azure-mgmt-rdbms==10.0.0 33 | azure-mgmt-search==8.0.0 34 | azure-mgmt-servicebus==7.1.0 35 | azure-mgmt-sql==3.0.1 36 | azure-mgmt-storage==19.0.0 37 | azure-mgmt-trafficmanager==1.0.0b1 38 | azure-mgmt-web==6.1.0 39 | azure-nspkg==2.0.0 40 | azure-storage-blob==12.11.0 41 | azure-core==1.28.0 42 | azure-keyvault==4.2.0 43 | azure-mgmt-keyvault==10.0.0 44 | azure-mgmt-cosmosdb==6.4.0 45 | azure-mgmt-hdinsight==9.0.0 46 | azure-mgmt-devtestlabs==9.0.0 47 | azure-mgmt-loganalytics==12.0.0 48 | azure-mgmt-automation==1.0.0 49 | azure-mgmt-iothub==2.2.0 50 | azure-iot-hub==2.6.1 51 | azure-mgmt-recoveryservices==2.0.0 52 | azure-mgmt-recoveryservicesbackup==3.0.0 53 | azure-mgmt-notificationhubs==7.0.0 54 | azure-mgmt-eventhub==10.1.0 55 | -------------------------------------------------------------------------------- /tests/integration/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: https://github.com/ansible-collections/amazon.aws.git 4 | type: git 5 | version: main 6 | - name: https://github.com/ansible-collections/community.aws.git 7 | type: git 8 | version: main 9 | - name: https://github.com/ansible-collections/azure.git 10 | type: git 11 | version: dev 12 | - name: https://github.com/ansible-collections/google.cloud.git 13 | type: git 14 | version: master 15 | -------------------------------------------------------------------------------- /tests/integration/targets/action_groups/files/nothing.tf: -------------------------------------------------------------------------------- 1 | output "my_output" { 2 | value = "hello" 3 | } 4 | -------------------------------------------------------------------------------- /tests/integration/targets/action_groups/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | test_basedir: "{{ test_basedir | default(output_dir) }}" 3 | 4 | - name: Copy terraform files to work space 5 | ansible.builtin.copy: 6 | src: "{{ item }}" 7 | dest: "{{ test_basedir }}/{{ item }}" 8 | loop: 9 | - nothing.tf 10 | 11 | - block: 12 | - name: Terraform in present non-check mode 13 | cloud.terraform.terraform: 14 | register: terraform_result 15 | check_mode: true 16 | - assert: 17 | that: 18 | - terraform_result is not failed 19 | - terraform_result is changed 20 | module_defaults: 21 | group/cloud.terraform.terraform: 22 | project_path: "{{ test_basedir }}" 23 | state: present 24 | force_init: true 25 | -------------------------------------------------------------------------------- /tests/integration/targets/aws/aliases: -------------------------------------------------------------------------------- 1 | cloud/aws 2 | -------------------------------------------------------------------------------- /tests/integration/targets/aws/files/cloud.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "=4.38.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | } 12 | 13 | variable "cloud_terraform_integration_id" { 14 | type = string 15 | } 16 | 17 | variable "cidr_block" { 18 | type = string 19 | } 20 | 21 | resource "aws_vpc" "test_vpc" { 22 | cidr_block = var.cidr_block 23 | tags = { 24 | Name = var.cloud_terraform_integration_id 25 | cloud_terraform_integration = "true" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/integration/targets/aws/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /tests/integration/targets/aws/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | - environment: 6 | AWS_ACCESS_KEY_ID: "{{ aws_access_key | default(omit) }}" 7 | AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key | default(omit) }}" 8 | AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" 9 | AWS_REGION: "{{ aws_region | default(omit) }}" 10 | 11 | block: 12 | - set_fact: 13 | test_basedir: "{{ test_basedir | default(output_dir) }}" 14 | resource_id: "{{ resource_prefix }}-vpc" 15 | vpc_cidr_block: '10.{{ 256 | random(seed=resource_prefix) }}.0.0/24' 16 | 17 | - name: Copy terraform files to workspace 18 | ansible.builtin.copy: 19 | src: "{{ item }}" 20 | dest: "{{ test_basedir }}/{{ item }}" 21 | loop: 22 | - cloud.tf 23 | 24 | - &verification 25 | block: 26 | - name: Fetch VPC info 27 | amazon.aws.ec2_vpc_net_info: 28 | filters: 29 | "tag:Name": "{{ resource_id }}" 30 | "cidr": "{{ vpc_cidr_block }}" 31 | register: vpc_info 32 | 33 | - name: Assert that there are {{ number_of_vpcs }} VPCs with tag:Name={{ resource_id }} and cidr={{ vpc_cidr_block }}' 34 | assert: 35 | that: 36 | - (vpc_info.vpcs | length) == number_of_vpcs 37 | - name: Assert that VPC {{ resource_id }} is present and the info matches 38 | assert: 39 | that: 40 | - vpc_info.vpcs[0].cidr_block == "{{ vpc_cidr_block }}" 41 | - vpc_info.vpcs[0].tags.Name == "{{ resource_id }}" 42 | when: number_of_vpcs == 1 43 | vars: 44 | number_of_vpcs: 0 45 | 46 | - name: Terraform in present check mode 47 | cloud.terraform.terraform: 48 | project_path: "{{ test_basedir }}" 49 | state: present 50 | force_init: true 51 | variables: 52 | cloud_terraform_integration_id: "{{ resource_id }}" 53 | cidr_block: "{{ vpc_cidr_block }}" 54 | register: terraform_result 55 | check_mode: true 56 | 57 | - assert: 58 | that: 59 | - terraform_result is not failed 60 | - terraform_result is changed 61 | 62 | - <<: *verification 63 | vars: 64 | number_of_vpcs: 0 65 | 66 | - name: Terraform in present non-check mode 67 | cloud.terraform.terraform: 68 | project_path: "{{ test_basedir }}" 69 | state: present 70 | force_init: true 71 | variables: 72 | cloud_terraform_integration_id: "{{ resource_id }}" 73 | cidr_block: "{{ vpc_cidr_block }}" 74 | register: terraform_result 75 | check_mode: false 76 | 77 | - assert: 78 | that: 79 | - terraform_result is not failed 80 | - terraform_result is changed 81 | 82 | - <<: *verification 83 | vars: 84 | number_of_vpcs: 1 85 | 86 | - name: Terraform in present non-check mode (idempotency) 87 | cloud.terraform.terraform: 88 | project_path: "{{ test_basedir }}" 89 | state: present 90 | force_init: true 91 | variables: 92 | cloud_terraform_integration_id: "{{ resource_id }}" 93 | cidr_block: "{{ vpc_cidr_block }}" 94 | register: terraform_result 95 | check_mode: false 96 | 97 | - assert: 98 | that: 99 | - terraform_result is not failed 100 | - terraform_result is not changed 101 | 102 | - <<: *verification 103 | vars: 104 | number_of_vpcs: 1 105 | 106 | - name: Terraform in absent check mode 107 | cloud.terraform.terraform: 108 | project_path: "{{ test_basedir }}" 109 | state: absent 110 | force_init: true 111 | variables: 112 | cloud_terraform_integration_id: "{{ resource_id }}" 113 | cidr_block: "{{ vpc_cidr_block }}" 114 | register: terraform_result 115 | check_mode: true 116 | 117 | - assert: 118 | that: 119 | - terraform_result is not failed 120 | - terraform_result is changed 121 | 122 | - <<: *verification 123 | vars: 124 | number_of_vpcs: 1 125 | 126 | - name: Terraform in absent non-check mode 127 | cloud.terraform.terraform: 128 | project_path: "{{ test_basedir }}" 129 | state: absent 130 | force_init: true 131 | variables: 132 | cloud_terraform_integration_id: "{{ resource_id }}" 133 | cidr_block: "{{ vpc_cidr_block }}" 134 | register: terraform_result 135 | check_mode: false 136 | 137 | - assert: 138 | that: 139 | - terraform_result is not failed 140 | - terraform_result is changed 141 | 142 | - <<: *verification 143 | vars: 144 | number_of_vpcs: 0 145 | 146 | # Clean up all integration test resources 147 | always: 148 | - name: Fetch VPC info 149 | amazon.aws.ec2_vpc_net_info: 150 | filters: 151 | "tag:Name": "{{ resource_id }}" 152 | "cidr": "{{ vpc_cidr_block }}" 153 | register: vpc_info 154 | ignore_errors: true 155 | 156 | - name: Delete VPC 157 | amazon.aws.ec2_vpc_net: 158 | vpc_id: "{{ item.vpc_id }}" 159 | state: absent 160 | loop: "{{ vpc_info.vpcs }}" 161 | ignore_errors: true 162 | -------------------------------------------------------------------------------- /tests/integration/targets/aws_sqs_queue/aliases: -------------------------------------------------------------------------------- 1 | cloud/aws 2 | -------------------------------------------------------------------------------- /tests/integration/targets/aws_sqs_queue/files/cloud.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "=4.38.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | } 12 | 13 | variable "cloud_terraform_integration_id" { 14 | type = string 15 | } 16 | 17 | output "aws_sqs_queue_tags" { 18 | description = "AWS SQS queue tags" 19 | value = resource.aws_sqs_queue.this.tags 20 | } 21 | 22 | output "aws_sqs_role_tags" { 23 | description = "AWS IAM role tags" 24 | value = resource.aws_iam_role.this.tags 25 | } 26 | 27 | resource "aws_sqs_queue" "this" { 28 | name = "${var.cloud_terraform_integration_id}-queue" 29 | tags = { 30 | Name = var.cloud_terraform_integration_id 31 | cloud_terraform_integration = "true" 32 | } 33 | } 34 | 35 | data "aws_iam_policy_document" "this" { 36 | statement { 37 | effect = "Allow" 38 | 39 | principals { 40 | type = "AWS" 41 | identifiers = [ 42 | aws_iam_role.this.arn 43 | ] 44 | } 45 | 46 | actions = [ 47 | "sqs:SendMessage" 48 | ] 49 | 50 | resources = [ 51 | aws_sqs_queue.this.arn 52 | ] 53 | } 54 | } 55 | 56 | resource "aws_sqs_queue_policy" "this" { 57 | queue_url = aws_sqs_queue.this.id 58 | policy = data.aws_iam_policy_document.this.json 59 | } 60 | 61 | resource "aws_iam_role" "this" { 62 | name = "${var.cloud_terraform_integration_id}-role" 63 | 64 | assume_role_policy = jsonencode({ 65 | Version = "2012-10-17" 66 | Statement = [{ 67 | Action = "sts:AssumeRole" 68 | Effect = "Allow" 69 | Sid = "" 70 | Principal = { 71 | Service = "lambda.amazonaws.com" 72 | } 73 | }] 74 | }) 75 | tags = { 76 | Name = var.cloud_terraform_integration_id 77 | cloud_terraform_integration = "true" 78 | } 79 | } 80 | 81 | resource "aws_iam_role_policy_attachment" "this" { 82 | role = aws_iam_role.this.name 83 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 84 | } 85 | -------------------------------------------------------------------------------- /tests/integration/targets/aws_sqs_queue/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /tests/integration/targets/aws_sqs_queue/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | - environment: 6 | AWS_ACCESS_KEY_ID: "{{ aws_access_key | default(omit) }}" 7 | AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key | default(omit) }}" 8 | AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" 9 | AWS_REGION: "{{ aws_region | default(omit) }}" 10 | 11 | block: 12 | - set_fact: 13 | test_basedir: "{{ test_basedir | default(output_dir) }}" 14 | resource_id: "{{ resource_prefix }}-sqs" 15 | - set_fact: 16 | test_queue_name: "{{ resource_id }}-queue" 17 | test_role_name: "{{ resource_id }}-role" 18 | 19 | - name: Copy terraform files to workspace 20 | ansible.builtin.copy: 21 | src: "{{ item }}" 22 | dest: "{{ test_basedir }}/{{ item }}" 23 | loop: 24 | - cloud.tf 25 | 26 | # iam_role_info - amazon.aws > 6.5.0 needed, tags need to be returned 27 | - &verification 28 | block: 29 | - name: Get terraform plan outputs 30 | cloud.terraform.terraform_output: 31 | project_path: "{{ test_basedir }}" 32 | register: tf_result 33 | - name: Assert returned output is empty 34 | assert: 35 | that: 36 | - tf_result.outputs == {} 37 | when: number_of_roles == 0 38 | - name: Assert returned output tags are set 39 | assert: 40 | that: 41 | - tf_result.outputs.aws_sqs_queue_tags.value.Name == resource_id 42 | - tf_result.outputs.aws_sqs_role_tags.value.Name == resource_id 43 | when: number_of_roles == 1 44 | vars: 45 | number_of_roles: 0 46 | 47 | - name: Terraform in present check mode 48 | cloud.terraform.terraform: 49 | project_path: "{{ test_basedir }}" 50 | state: present 51 | force_init: true 52 | variables: 53 | cloud_terraform_integration_id: "{{ resource_id }}" 54 | register: terraform_result 55 | check_mode: true 56 | 57 | - assert: 58 | that: 59 | - terraform_result is not failed 60 | - terraform_result is changed 61 | 62 | - <<: *verification 63 | vars: 64 | number_of_roles: 0 65 | 66 | - name: Terraform in present non-check mode 67 | cloud.terraform.terraform: 68 | project_path: "{{ test_basedir }}" 69 | state: present 70 | force_init: true 71 | variables: 72 | cloud_terraform_integration_id: "{{ resource_id }}" 73 | register: terraform_result 74 | check_mode: false 75 | 76 | - assert: 77 | that: 78 | - terraform_result is not failed 79 | - terraform_result is changed 80 | 81 | - <<: *verification 82 | vars: 83 | number_of_roles: 1 84 | 85 | - name: Terraform in present non-check mode (idempotency) 86 | cloud.terraform.terraform: 87 | project_path: "{{ test_basedir }}" 88 | state: present 89 | force_init: true 90 | variables: 91 | cloud_terraform_integration_id: "{{ resource_id }}" 92 | register: terraform_result 93 | check_mode: false 94 | 95 | - assert: 96 | that: 97 | - terraform_result is not failed 98 | - terraform_result is not changed 99 | 100 | - <<: *verification 101 | vars: 102 | number_of_roles: 1 103 | 104 | - name: Terraform in absent check mode 105 | cloud.terraform.terraform: 106 | project_path: "{{ test_basedir }}" 107 | state: absent 108 | force_init: true 109 | variables: 110 | cloud_terraform_integration_id: "{{ resource_id }}" 111 | register: terraform_result 112 | check_mode: true 113 | 114 | - assert: 115 | that: 116 | - terraform_result is not failed 117 | - terraform_result is changed 118 | 119 | - <<: *verification 120 | vars: 121 | number_of_roles: 1 122 | 123 | - name: Terraform in absent non-check mode 124 | cloud.terraform.terraform: 125 | project_path: "{{ test_basedir }}" 126 | state: absent 127 | force_init: true 128 | variables: 129 | cloud_terraform_integration_id: "{{ resource_id }}" 130 | register: terraform_result 131 | check_mode: false 132 | 133 | - assert: 134 | that: 135 | - terraform_result is not failed 136 | - terraform_result is changed 137 | 138 | - <<: *verification 139 | vars: 140 | number_of_roles: 0 141 | 142 | # Clean up all integration test resources 143 | always: 144 | - name: Delete SQS queue 145 | community.aws.sqs_queue: 146 | name: "{{ test_queue_name }}" 147 | state: absent 148 | ignore_errors: true 149 | 150 | - name: Delete IAM role 151 | amazon.aws.iam_role: 152 | name: "{{ test_role_name }}" 153 | state: absent 154 | ignore_errors: true 155 | -------------------------------------------------------------------------------- /tests/integration/targets/awscc/aliases: -------------------------------------------------------------------------------- 1 | cloud/aws 2 | -------------------------------------------------------------------------------- /tests/integration/targets/awscc/files/cloud.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | awscc = { 4 | source = "hashicorp/awscc" 5 | version = "=0.63.0" 6 | } 7 | } 8 | } 9 | 10 | provider "awscc" { 11 | } 12 | 13 | variable "vpc_id" { 14 | type = string 15 | } 16 | 17 | variable "cidr_block" { 18 | type = string 19 | } 20 | 21 | resource "awscc_ec2_subnet" "main" { 22 | vpc_id = var.vpc_id 23 | cidr_block = var.cidr_block 24 | } 25 | -------------------------------------------------------------------------------- /tests/integration/targets/awscc/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | - environment: 6 | AWS_ACCESS_KEY_ID: "{{ aws_access_key | default(omit) }}" 7 | AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key | default(omit) }}" 8 | AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" 9 | AWS_REGION: "{{ aws_region | default(omit) }}" 10 | 11 | block: 12 | - set_fact: 13 | test_basedir: "{{ test_basedir | default(output_dir) }}" 14 | vpc_name: "{{ resource_prefix }}-awscc-vpc" 15 | vpc_cidr_block: '10.{{ 256 | random(seed=resource_prefix) }}.0.0/24' 16 | 17 | - name: Copy terraform files to workspace 18 | ansible.builtin.copy: 19 | src: "{{ item }}" 20 | dest: "{{ test_basedir }}/{{ item }}" 21 | loop: 22 | - cloud.tf 23 | 24 | - name: Create VPC 25 | amazon.aws.ec2_vpc_net: 26 | name: "{{ vpc_name }}" 27 | cidr_block: 28 | - "{{ vpc_cidr_block }}" 29 | state: present 30 | register: vpc_info 31 | 32 | - set_fact: 33 | vpc_id: "{{ vpc_info.vpc.id }}" 34 | 35 | - &verification 36 | block: 37 | - name: Fetch Subnet info 38 | amazon.aws.ec2_vpc_subnet_info: 39 | filters: 40 | vpc-id: "{{ vpc_id }}" 41 | cidr-block: "{{ vpc_cidr_block }}" 42 | register: subnet_info 43 | 44 | - name: Assert that there are {{ number_of_subnets }} Subnets 45 | assert: 46 | that: 47 | - (subnet_info.subnets | length) == number_of_subnets 48 | - name: Assert that Subnet is present and the info matches 49 | assert: 50 | that: 51 | - subnet_info.subnets[0].cidr_block == vpc_cidr_block 52 | when: number_of_subnets == 1 53 | vars: 54 | number_of_subnets: 0 55 | 56 | - name: Terraform in present check mode 57 | cloud.terraform.terraform: 58 | project_path: "{{ test_basedir }}" 59 | state: present 60 | force_init: true 61 | variables: 62 | vpc_id: "{{ vpc_id }}" 63 | cidr_block: "{{ vpc_cidr_block }}" 64 | register: terraform_result 65 | check_mode: true 66 | 67 | - assert: 68 | that: 69 | - terraform_result is not failed 70 | - terraform_result is changed 71 | 72 | - <<: *verification 73 | vars: 74 | number_of_subnets: 0 75 | 76 | - name: Terraform in present non-check mode 77 | cloud.terraform.terraform: 78 | project_path: "{{ test_basedir }}" 79 | state: present 80 | force_init: true 81 | variables: 82 | vpc_id: "{{ vpc_id }}" 83 | cidr_block: "{{ vpc_cidr_block }}" 84 | register: terraform_result 85 | check_mode: false 86 | 87 | - assert: 88 | that: 89 | - terraform_result is not failed 90 | - terraform_result is changed 91 | 92 | - <<: *verification 93 | vars: 94 | number_of_subnets: 1 95 | 96 | - name: Terraform in present non-check mode (idempotency) 97 | cloud.terraform.terraform: 98 | project_path: "{{ test_basedir }}" 99 | state: present 100 | force_init: true 101 | variables: 102 | vpc_id: "{{ vpc_id }}" 103 | cidr_block: "{{ vpc_cidr_block }}" 104 | register: terraform_result 105 | check_mode: false 106 | 107 | - assert: 108 | that: 109 | - terraform_result is not failed 110 | - terraform_result is not changed 111 | 112 | - <<: *verification 113 | vars: 114 | number_of_subnets: 1 115 | 116 | - name: Terraform in absent check mode 117 | cloud.terraform.terraform: 118 | project_path: "{{ test_basedir }}" 119 | state: absent 120 | force_init: true 121 | variables: 122 | vpc_id: "{{ vpc_id }}" 123 | cidr_block: "{{ vpc_cidr_block }}" 124 | register: terraform_result 125 | check_mode: true 126 | 127 | - assert: 128 | that: 129 | - terraform_result is not failed 130 | - terraform_result is changed 131 | 132 | - <<: *verification 133 | vars: 134 | number_of_subnets: 1 135 | 136 | - name: Terraform in absent non-check mode 137 | cloud.terraform.terraform: 138 | project_path: "{{ test_basedir }}" 139 | state: absent 140 | force_init: true 141 | variables: 142 | vpc_id: "{{ vpc_id }}" 143 | cidr_block: "{{ vpc_cidr_block }}" 144 | register: terraform_result 145 | check_mode: false 146 | 147 | - assert: 148 | that: 149 | - terraform_result is not failed 150 | - terraform_result is changed 151 | 152 | - <<: *verification 153 | vars: 154 | number_of_subnets: 0 155 | 156 | # Clean up all integration test resources 157 | always: 158 | - name: Fetch Subnet info 159 | amazon.aws.ec2_vpc_subnet_info: 160 | filters: 161 | vpc-id: "{{ vpc_id }}" 162 | cidr-block: "{{ vpc_cidr_block }}" 163 | register: subnet_info 164 | ignore_errors: true 165 | 166 | - name: Delete Subnets 167 | amazon.aws.ec2_vpc_subnet: 168 | cidr: "{{ item.cidr_block }}" 169 | vpc_id: "{{ vpc_id }}" 170 | wait: true 171 | state: absent 172 | with_items: "{{ subnet_info.subnets }}" 173 | ignore_errors: true 174 | 175 | - name: Delete VPC 176 | amazon.aws.ec2_vpc_net: 177 | name: "{{ vpc_name }}" 178 | cidr_block: 179 | - "{{ vpc_cidr_block }}" 180 | state: absent 181 | ignore_errors: true 182 | -------------------------------------------------------------------------------- /tests/integration/targets/azure/aliases: -------------------------------------------------------------------------------- 1 | # reason: missing policy and credentials 2 | unsupported 3 | -------------------------------------------------------------------------------- /tests/integration/targets/azure/files/cloud.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azurerm = { 4 | source = "hashicorp/azurerm" 5 | version = "=3.30.0" 6 | } 7 | } 8 | } 9 | 10 | provider "azurerm" { 11 | features {} 12 | skip_provider_registration = true 13 | } 14 | 15 | variable "cloud_terraform_integration_id" { 16 | type = string 17 | } 18 | 19 | variable "cloud_terraform_integration_resource_group" { 20 | type = string 21 | } 22 | 23 | variable "cloud_terraform_integration_location" { 24 | type = string 25 | } 26 | 27 | resource "azurerm_virtual_network" "test_vnet" { 28 | # ansible uses names as identifiers, so this must be generated 29 | name = var.cloud_terraform_integration_id 30 | resource_group_name = var.cloud_terraform_integration_resource_group 31 | location = var.cloud_terraform_integration_location 32 | address_space = ["10.11.12.0/24"] 33 | tags = { 34 | cloud_terraform_integration = "true" 35 | cloud_terraform_integration_id = var.cloud_terraform_integration_id 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/integration/targets/azure/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - set_fact: 7 | test_basedir: "{{ test_basedir | default(output_dir) }}" 8 | resource_id: "{{ resource_prefix }}-vnet" 9 | resource_group_name: "cloud.terraform_integration_resource_group" 10 | resource_group_location: "northeurope" 11 | 12 | - name: Copy terraform files to workspace 13 | ansible.builtin.copy: 14 | src: "{{ item }}" 15 | dest: "{{ test_basedir }}/{{ item }}" 16 | loop: 17 | - cloud.tf 18 | 19 | - name: Create a resource group 20 | azure.azcollection.azure_rm_resourcegroup: 21 | name: "{{ resource_group_name }}" 22 | location: "{{ resource_group_location }}" 23 | tags: 24 | cloud_terraform_integration: "true" 25 | 26 | - name: Clean up all integration test resources 27 | block: 28 | - name: Get a list of vnets with a matching tag 29 | azure.azcollection.azure_rm_virtualnetwork_info: 30 | tags: 31 | - "cloud_terraform_integration:true" 32 | register: integration_vnets 33 | 34 | - name: Delete vnets 35 | azure.azcollection.azure_rm_virtualnetwork: 36 | name: "{{ item.name }}" 37 | resource_group: "{{ resource_group_name }}" 38 | state: absent 39 | loop: "{{ integration_vnets.virtualnetworks }}" 40 | 41 | - &verification 42 | block: 43 | - name: Get a list of vnets with a matching tag 44 | azure.azcollection.azure_rm_virtualnetwork_info: 45 | tags: 46 | - "cloud_terraform_integration_id:{{ resource_id }}" 47 | register: vnet_info 48 | - name: Assert that there are {{ number_of_vnets }} vnets present 49 | assert: 50 | that: 51 | - (vnet_info.virtualnetworks | length) == number_of_vnets 52 | vars: 53 | number_of_vnets: 0 54 | 55 | - name: Terraform in present check mode 56 | cloud.terraform.terraform: 57 | project_path: "{{ test_basedir }}" 58 | state: present 59 | force_init: true 60 | variables: 61 | cloud_terraform_integration_id: "{{ resource_id }}" 62 | cloud_terraform_integration_resource_group: "{{ resource_group_name }}" 63 | cloud_terraform_integration_location: "{{ resource_group_location }}" 64 | register: terraform_result 65 | check_mode: true 66 | - assert: 67 | that: 68 | - terraform_result is not failed 69 | - terraform_result is changed 70 | - <<: *verification 71 | vars: 72 | number_of_vnets: 0 73 | 74 | - name: Terraform in present non-check mode 75 | cloud.terraform.terraform: 76 | project_path: "{{ test_basedir }}" 77 | state: present 78 | force_init: true 79 | variables: 80 | cloud_terraform_integration_id: "{{ resource_id }}" 81 | cloud_terraform_integration_resource_group: "{{ resource_group_name }}" 82 | cloud_terraform_integration_location: "{{ resource_group_location }}" 83 | register: terraform_result 84 | check_mode: false 85 | - assert: 86 | that: 87 | - terraform_result is not failed 88 | - terraform_result is changed 89 | - <<: *verification 90 | vars: 91 | number_of_vnets: 1 92 | 93 | - name: Terraform in present non-check mode (idempotency) 94 | cloud.terraform.terraform: 95 | project_path: "{{ test_basedir }}" 96 | state: present 97 | force_init: true 98 | variables: 99 | cloud_terraform_integration_id: "{{ resource_id }}" 100 | cloud_terraform_integration_resource_group: "{{ resource_group_name }}" 101 | cloud_terraform_integration_location: "{{ resource_group_location }}" 102 | register: terraform_result 103 | check_mode: false 104 | - assert: 105 | that: 106 | - terraform_result is not failed 107 | - terraform_result is not changed 108 | - <<: *verification 109 | vars: 110 | number_of_vnets: 1 111 | 112 | - name: Terraform in absent check mode 113 | cloud.terraform.terraform: 114 | project_path: "{{ test_basedir }}" 115 | state: absent 116 | force_init: true 117 | variables: 118 | cloud_terraform_integration_id: "{{ resource_id }}" 119 | cloud_terraform_integration_resource_group: "{{ resource_group_name }}" 120 | cloud_terraform_integration_location: "{{ resource_group_location }}" 121 | register: terraform_result 122 | check_mode: true 123 | - assert: 124 | that: 125 | - terraform_result is not failed 126 | - terraform_result is changed 127 | - <<: *verification 128 | vars: 129 | number_of_vnets: 1 130 | 131 | - name: Terraform in absent non-check mode 132 | cloud.terraform.terraform: 133 | project_path: "{{ test_basedir }}" 134 | state: absent 135 | force_init: true 136 | variables: 137 | cloud_terraform_integration_id: "{{ resource_id }}" 138 | cloud_terraform_integration_resource_group: "{{ resource_group_name }}" 139 | cloud_terraform_integration_location: "{{ resource_group_location }}" 140 | register: terraform_result 141 | check_mode: false 142 | - assert: 143 | that: 144 | - terraform_result is not failed 145 | - terraform_result is changed 146 | - <<: *verification 147 | vars: 148 | number_of_vnets: 0 149 | -------------------------------------------------------------------------------- /tests/integration/targets/complex_variables/files/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ansible Project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | resource "null_resource" "mynullresource" { 6 | triggers = { 7 | # plain dictionaries 8 | dict_name = var.dictionaries.name 9 | dict_age = var.dictionaries.age 10 | 11 | # list of dicrs 12 | join_dic_name = join(",", var.list_of_objects.*.name) 13 | 14 | # list-of-strings 15 | join_list = join(",", var.list_of_strings.*) 16 | 17 | # testing boolean 18 | name = var.boolean ? var.dictionaries.name : var.list_of_objects[0].name 19 | 20 | # top level string 21 | sample_string_1 = var.string_type 22 | 23 | # nested lists 24 | num_from_matrix = var.list_of_lists[1][2] 25 | } 26 | 27 | } 28 | 29 | output "string_type" { 30 | value = var.string_type 31 | } 32 | 33 | output "multiline_string" { 34 | value = var.multiline_string 35 | } 36 | -------------------------------------------------------------------------------- /tests/integration/targets/complex_variables/files/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ansible Project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | variable "dictionaries" { 6 | type = object({ 7 | name = string 8 | age = number 9 | }) 10 | description = "Same as ansible Dict" 11 | default = { 12 | age = 1 13 | name = "value" 14 | } 15 | } 16 | 17 | variable "list_of_strings" { 18 | type = list(string) 19 | description = "list of strings" 20 | validation { 21 | condition = (var.list_of_strings[1] == "cli specials\"&$%@#*!(){}[]:\"\" \\\\") 22 | error_message = "Strings do not match." 23 | } 24 | } 25 | 26 | variable "list_of_objects" { 27 | type = list(object({ 28 | name = string 29 | age = number 30 | })) 31 | validation { 32 | condition = (var.list_of_objects[1].name == "cli specials\"&$%@#*!(){}[]:\"\" \\\\") 33 | error_message = "Strings do not match." 34 | } 35 | } 36 | 37 | variable "boolean" { 38 | type = bool 39 | description = "boolean" 40 | 41 | } 42 | 43 | variable "string_type" { 44 | type = string 45 | validation { 46 | condition = (var.string_type == "cli specials\"&$%@#*!(){}[]:\"\" \\\\") 47 | error_message = "Strings do not match." 48 | } 49 | } 50 | 51 | variable "multiline_string" { 52 | type = string 53 | validation { 54 | condition = (var.multiline_string == "one\ntwo\n") 55 | error_message = "Strings do not match." 56 | } 57 | } 58 | 59 | variable "list_of_lists" { 60 | type = list(list(any)) 61 | default = [ [ 1 ], [1, 2, 3], [3] ] 62 | } 63 | -------------------------------------------------------------------------------- /tests/integration/targets/complex_variables/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - name: Create terraform project directory (complex variables) 7 | ansible.builtin.file: 8 | path: "/tmp/complex_vars" 9 | state: directory 10 | mode: 0755 11 | 12 | - name: copy terraform files to work space 13 | ansible.builtin.copy: 14 | src: "{{ item }}" 15 | dest: "/tmp/complex_vars/{{ item }}" 16 | with_items: 17 | - main.tf 18 | - variables.tf 19 | 20 | # This task would test the various complex variable structures of the with the 21 | # terraform null_resource 22 | - name: test complex variables 23 | cloud.terraform.terraform: 24 | project_path: "/tmp/complex_vars" 25 | force_init: true 26 | complex_vars: true 27 | variables: 28 | dictionaries: 29 | name: "kosala" 30 | age: 99 31 | list_of_strings: 32 | - "kosala" 33 | - 'cli specials"&$%@#*!(){}[]:"" \\' 34 | - "xxx" 35 | - "zzz" 36 | list_of_objects: 37 | - name: "kosala" 38 | age: 99 39 | - name: 'cli specials"&$%@#*!(){}[]:"" \\' 40 | age: 0.1 41 | - name: "zzz" 42 | age: 9.789 43 | - name: "lll" 44 | age: 1000 45 | boolean: true 46 | string_type: 'cli specials"&$%@#*!(){}[]:"" \\' 47 | multiline_string: | 48 | one 49 | two 50 | list_of_lists: 51 | - [ 1 ] 52 | - [ 11, 12, 13 ] 53 | - [ 2 ] 54 | - [ 3 ] 55 | state: present 56 | register: terraform_init_result 57 | 58 | - assert: 59 | that: terraform_init_result is not failed 60 | -------------------------------------------------------------------------------- /tests/integration/targets/gcp/aliases: -------------------------------------------------------------------------------- 1 | # reason: missing policy and credentials 2 | unsupported 3 | -------------------------------------------------------------------------------- /tests/integration/targets/gcp/files/cloud.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "=4.43.0" 6 | } 7 | } 8 | } 9 | 10 | # the provider can't function with an implicit project from application-default credentials 11 | variable "cloud_terraform_integration_project" { 12 | type = string 13 | } 14 | 15 | provider "google" { 16 | project = var.cloud_terraform_integration_project 17 | } 18 | 19 | variable "cloud_terraform_integration_id" { 20 | type = string 21 | } 22 | 23 | resource "google_compute_network" "test_vpc" { 24 | name = var.cloud_terraform_integration_id 25 | } 26 | -------------------------------------------------------------------------------- /tests/integration/targets/gcp/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | # the gcloud modules and terraform provider can't use the project implicitly specified in the ADC 7 | # we get the configured project directly from the credentials file 8 | # we abuse the variable loader for this 9 | - include_vars: 10 | name: gcloud_auth 11 | file: "~/.config/gcloud/application_default_credentials.json" 12 | no_log: true 13 | 14 | - set_fact: 15 | test_basedir: "{{ test_basedir | default(output_dir) }}" 16 | # project names must match this regex, but can start with a number by default 17 | # "^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$" 18 | resource_id: "{{ resource_prefix }}-vpc" 19 | resource_project: "{{ gcloud_auth.quota_project_id }}" 20 | 21 | - name: Copy terraform files to workspace 22 | ansible.builtin.copy: 23 | src: "{{ item }}" 24 | dest: "{{ test_basedir }}/{{ item }}" 25 | loop: 26 | - cloud.tf 27 | 28 | - name: Clean up all integration test resources 29 | block: 30 | - name: Get a list of VPCs with a matching tag 31 | google.cloud.gcp_compute_network_info: 32 | auth_kind: application 33 | project: "{{ resource_project }}" 34 | filters: 35 | - "name = {{ resource_id }}" 36 | register: integration_vpcs 37 | 38 | - name: Delete VPCs 39 | google.cloud.gcp_compute_network: 40 | auth_kind: application 41 | project: "{{ resource_project }}" 42 | name: "{{ item.name }}" 43 | state: absent 44 | loop: "{{ integration_vpcs.resources }}" 45 | 46 | - &verification 47 | block: 48 | - name: Get a list of VPCs with a matching tag 49 | google.cloud.gcp_compute_network_info: 50 | auth_kind: application 51 | project: "{{ resource_project }}" 52 | filters: 53 | - "name = {{ resource_id }}" 54 | register: vpc_info 55 | - name: Assert that there are {{ number_of_vpcs }} VPCs present 56 | assert: 57 | that: 58 | - (vpc_info.resources | length) == number_of_vpcs 59 | vars: 60 | number_of_vpcs: 0 61 | 62 | - name: Terraform in present check mode 63 | cloud.terraform.terraform: 64 | project_path: "{{ test_basedir }}" 65 | state: present 66 | force_init: true 67 | variables: 68 | cloud_terraform_integration_id: "{{ resource_id }}" 69 | cloud_terraform_integration_project: "{{ resource_project }}" 70 | register: terraform_result 71 | check_mode: true 72 | - assert: 73 | that: 74 | - terraform_result is not failed 75 | - terraform_result is changed 76 | - <<: *verification 77 | vars: 78 | number_of_vpcs: 0 79 | 80 | - name: Terraform in present non-check mode 81 | cloud.terraform.terraform: 82 | project_path: "{{ test_basedir }}" 83 | state: present 84 | force_init: true 85 | variables: 86 | cloud_terraform_integration_id: "{{ resource_id }}" 87 | cloud_terraform_integration_project: "{{ resource_project }}" 88 | register: terraform_result 89 | check_mode: false 90 | - assert: 91 | that: 92 | - terraform_result is not failed 93 | - terraform_result is changed 94 | - <<: *verification 95 | vars: 96 | number_of_vpcs: 1 97 | 98 | - name: Terraform in present non-check mode (idempotency) 99 | cloud.terraform.terraform: 100 | project_path: "{{ test_basedir }}" 101 | state: present 102 | force_init: true 103 | variables: 104 | cloud_terraform_integration_id: "{{ resource_id }}" 105 | cloud_terraform_integration_project: "{{ resource_project }}" 106 | register: terraform_result 107 | check_mode: false 108 | - assert: 109 | that: 110 | - terraform_result is not failed 111 | - terraform_result is not changed 112 | - <<: *verification 113 | vars: 114 | number_of_vpcs: 1 115 | 116 | - name: Terraform in absent check mode 117 | cloud.terraform.terraform: 118 | project_path: "{{ test_basedir }}" 119 | state: absent 120 | force_init: true 121 | variables: 122 | cloud_terraform_integration_id: "{{ resource_id }}" 123 | cloud_terraform_integration_project: "{{ resource_project }}" 124 | register: terraform_result 125 | check_mode: true 126 | - assert: 127 | that: 128 | - terraform_result is not failed 129 | - terraform_result is changed 130 | - <<: *verification 131 | vars: 132 | number_of_vpcs: 1 133 | 134 | - name: Terraform in absent non-check mode 135 | cloud.terraform.terraform: 136 | project_path: "{{ test_basedir }}" 137 | state: absent 138 | force_init: true 139 | variables: 140 | cloud_terraform_integration_id: "{{ resource_id }}" 141 | cloud_terraform_integration_project: "{{ resource_project }}" 142 | register: terraform_result 143 | check_mode: false 144 | - assert: 145 | that: 146 | - terraform_result is not failed 147 | - terraform_result is changed 148 | - <<: *verification 149 | vars: 150 | number_of_vpcs: 0 151 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/aliases: -------------------------------------------------------------------------------- 1 | cloud/aws -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/runme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | function cleanup() { 6 | rm -rf test.terraform_state.yml aws_credentials.sh 7 | ansible-playbook teardown.yml "$@" 8 | exit 1 9 | } 10 | 11 | trap 'cleanup "${@}"' ERR 12 | 13 | # Create ec2 instances 14 | ansible-playbook setup.yml "$@" 15 | 16 | export ANSIBLE_INVENTORY_ENABLED="cloud.terraform.terraform_state" 17 | 18 | export ANSIBLE_INVENTORY=test.terraform_state.yml 19 | 20 | set +x 21 | source aws_credentials.sh 22 | set -x 23 | 24 | ansible-playbook test.yml "$@" 25 | 26 | # Delete ec2 instances 27 | ansible-playbook teardown.yml "$@" 28 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/setup.yml: -------------------------------------------------------------------------------- 1 | - name: Create aws resources 2 | hosts: localhost 3 | gather_facts: false 4 | 5 | module_defaults: 6 | group/aws: 7 | access_key: '{{ aws_access_key }}' 8 | secret_key: '{{ aws_secret_key }}' 9 | session_token: '{{ security_token | default(omit) }}' 10 | region: '{{ aws_region }}' 11 | 12 | environment: 13 | AWS_ACCESS_KEY_ID: "{{ aws_access_key | default(omit) }}" 14 | AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key | default(omit) }}" 15 | AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" 16 | AWS_REGION: "{{ aws_region | default(omit) }}" 17 | 18 | vars_files: 19 | - vars/main.yml 20 | 21 | tasks: 22 | - name: Create aws resource 23 | block: 24 | - name: Create s3 bucket for testing 25 | amazon.aws.s3_bucket: 26 | name: "{{ bucket_name }}" 27 | 28 | - name: Create temporary directory to store main.tf configuration 29 | tempfile: 30 | state: directory 31 | suffix: .terraform 32 | register: temp_dir 33 | 34 | - include_tasks: tasks/generate_tf.yml 35 | vars: 36 | terraform_src_dir: "{{ temp_dir.path }}" 37 | 38 | - name: Create terraform resource 39 | cloud.terraform.terraform: 40 | project_path: "{{ temp_dir.path }}" 41 | force_init: true 42 | 43 | - name: Delete temporary directory 44 | file: 45 | state: absent 46 | path: "{{ temp_dir.path }}" 47 | when: temp_dir is defined 48 | 49 | - name: Generate credentials file 50 | template: 51 | src: 'aws_credentials.sh.j2' 52 | dest: 'aws_credentials.sh' 53 | mode: '0755' 54 | 55 | rescue: 56 | - name: Delete s3 bucket for testing 57 | amazon.aws.s3_bucket: 58 | name: "{{ bucket_name }}" 59 | state: absent 60 | force: true 61 | 62 | - name: Delete temporary directory 63 | file: 64 | state: absent 65 | path: "{{ temp_dir.path }}" 66 | when: temp_dir is defined 67 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/tasks/generate_tf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create child module directory 3 | ansible.builtin.file: 4 | state: directory 5 | path: "{{ terraform_src_dir }}/{{ child_module_path }}" 6 | 7 | - name: Generate Terraform configuration 8 | template: 9 | src: "{{ item.src }}" 10 | dest: "{{ item.dest }}" 11 | with_items: 12 | - src: main.tf.j2 13 | dest: "{{ terraform_src_dir }}/main.tf" 14 | - src: main.child.tf.j2 15 | dest: "{{ terraform_src_dir }}/{{ child_module_path }}/main.tf" -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/teardown.yml: -------------------------------------------------------------------------------- 1 | - name: Delete aws resources 2 | hosts: localhost 3 | gather_facts: false 4 | 5 | module_defaults: 6 | group/aws: 7 | access_key: '{{ aws_access_key }}' 8 | secret_key: '{{ aws_secret_key }}' 9 | session_token: '{{ security_token | default(omit) }}' 10 | region: '{{ aws_region }}' 11 | 12 | environment: 13 | AWS_ACCESS_KEY_ID: "{{ aws_access_key | default(omit) }}" 14 | AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key | default(omit) }}" 15 | AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" 16 | AWS_REGION: "{{ aws_region | default(omit) }}" 17 | 18 | vars_files: 19 | - vars/main.yml 20 | 21 | tasks: 22 | - name: Delete testing resources from aws 23 | block: 24 | - name: Create temporary directory to store main.tf configuration 25 | tempfile: 26 | state: directory 27 | suffix: .terraform 28 | register: temp_dir 29 | 30 | - include_tasks: tasks/generate_tf.yml 31 | vars: 32 | terraform_src_dir: "{{ temp_dir.path }}" 33 | 34 | - name: Destroy terraform resource 35 | cloud.terraform.terraform: 36 | project_path: "{{ temp_dir.path }}" 37 | force_init: true 38 | state: absent 39 | 40 | - name: Delete s3 bucket for testing 41 | amazon.aws.s3_bucket: 42 | name: "{{ bucket_name }}" 43 | state: absent 44 | force: true 45 | 46 | rescue: 47 | - name: Delete s3 bucket for testing 48 | amazon.aws.s3_bucket: 49 | name: "{{ bucket_name }}" 50 | state: absent 51 | force: true 52 | 53 | - name: Delete credentials file 54 | file: 55 | state: absent 56 | path: "aws_credentials.sh" 57 | ignore_errors: true 58 | 59 | - name: Delete temporary directory 60 | file: 61 | state: absent 62 | path: "{{ temp_dir.path }}" 63 | when: temp_dir is defined 64 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/aws_credentials.sh.j2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | {% if aws_access_key is defined %} 4 | export AWS_ACCESS_KEY_ID="{{ aws_access_key }}" 5 | {% endif %} 6 | {% if aws_secret_key is defined %} 7 | export AWS_SECRET_ACCESS_KEY="{{ aws_secret_key }}" 8 | {% endif %} 9 | {% if security_token is defined %} 10 | export AWS_SESSION_TOKEN="{{ security_token }}" 11 | {% endif %} 12 | {% if aws_region is defined %} 13 | export AWS_REGION="{{ aws_region }}" 14 | {% endif %} -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/backend.hcl.j2: -------------------------------------------------------------------------------- 1 | bucket = "{{ bucket_name }}" 2 | key = "ansible/terraform.tfstate" 3 | region = "{{ aws_region }}" 4 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/inventory.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: s3 4 | backend_config: 5 | bucket: {{ bucket_name }} 6 | key: ansible/terraform.tfstate 7 | region: {{ aws_region }} -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/inventory_search_child_module.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: s3 4 | backend_config: 5 | bucket: {{ bucket_name }} 6 | key: ansible/terraform.tfstate 7 | region: {{ aws_region }} 8 | search_child_modules: true -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_backend_files.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: s3 4 | backend_config_files: {{ backend_config_files }} 5 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_compose.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: s3 4 | backend_config: 5 | bucket: {{ bucket_name }} 6 | key: ansible/terraform.tfstate 7 | region: {{ aws_region }} 8 | compose: 9 | ansible_host: 'private_ip' -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_constructed.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: s3 4 | backend_config: 5 | bucket: {{ bucket_name }} 6 | key: ansible/terraform.tfstate 7 | region: {{ aws_region }} 8 | keyed_groups: 9 | - key: instance_state 10 | prefix: state 11 | - prefix: tag 12 | key: tags 13 | groups: 14 | no_public_ip: public_ip == "" 15 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_hostname.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: s3 4 | backend_config: 5 | bucket: {{ bucket_name }} 6 | key: ansible/terraform.tfstate 7 | region: {{ aws_region }} 8 | hostnames: 9 | - name: 'tag:Name' 10 | separator: "-" 11 | prefix: 'instance_type' -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/main.child.tf.j2: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | } 12 | 13 | variable "child_ami_id" { 14 | type = string 15 | } 16 | 17 | variable "child_subnet_id" { 18 | type = string 19 | } 20 | 21 | variable "child_group_id" { 22 | type = string 23 | } 24 | 25 | resource "aws_instance" "test_tiny" { 26 | ami = var.child_ami_id 27 | instance_type = "t3.micro" 28 | subnet_id = var.child_subnet_id 29 | vpc_security_group_ids = [var.child_group_id] 30 | 31 | tags = { 32 | Name = "{{ resource_prefix }}-another-ec2" 33 | } 34 | } -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/templates/main.tf.j2: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.0" 6 | } 7 | } 8 | backend "s3" { 9 | bucket = "{{ bucket_name }}" 10 | key = "ansible/terraform.tfstate" 11 | region = "{{ aws_region }}" 12 | } 13 | } 14 | 15 | provider "aws" { 16 | } 17 | 18 | data "aws_ssm_parameter" "amazon_ami" { 19 | name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" 20 | } 21 | 22 | resource "aws_vpc" "main" { 23 | cidr_block = "168.10.0.0/16" 24 | 25 | tags = { 26 | Name = "vpc-{{ resource_prefix }}" 27 | } 28 | } 29 | 30 | resource "aws_subnet" "main" { 31 | vpc_id = aws_vpc.main.id 32 | cidr_block = "168.10.1.0/24" 33 | 34 | tags = { 35 | Name = "subnet-{{ resource_prefix }}" 36 | } 37 | } 38 | 39 | resource "aws_security_group" "allow_ssh" { 40 | name = "allow_ssh" 41 | description = "Allow SSH inbound traffic" 42 | vpc_id = aws_vpc.main.id 43 | 44 | ingress { 45 | description = "SSH connect" 46 | from_port = 22 47 | to_port = 22 48 | protocol = "tcp" 49 | cidr_blocks = ["0.0.0.0/0"] 50 | ipv6_cidr_blocks = ["::/0"] 51 | } 52 | 53 | egress { 54 | from_port = 0 55 | to_port = 0 56 | protocol = "-1" 57 | cidr_blocks = ["0.0.0.0/0"] 58 | ipv6_cidr_blocks = ["::/0"] 59 | } 60 | 61 | tags = { 62 | Name = "allow_ssh_{{ resource_prefix }}" 63 | } 64 | } 65 | 66 | resource "aws_instance" "test" { 67 | ami = data.aws_ssm_parameter.amazon_ami.value 68 | instance_type = "t2.micro" 69 | subnet_id = aws_subnet.main.id 70 | vpc_security_group_ids = [aws_security_group.allow_ssh.id] 71 | 72 | tags = { 73 | Name = "{{ resource_prefix }}-ec2" 74 | Inventory = "terraform_state" 75 | Phase = "integration" 76 | } 77 | } 78 | 79 | module "child_module" { 80 | source = "./{{ child_module_path }}" 81 | child_ami_id = data.aws_ssm_parameter.amazon_ami.value 82 | child_group_id = aws_security_group.allow_ssh.id 83 | child_subnet_id = aws_subnet.main.id 84 | } -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | gather_facts: false 4 | 5 | vars_files: 6 | - vars/main.yml 7 | 8 | tasks: 9 | - name: Remove existing inventory file 10 | file: 11 | state: absent 12 | path: "{{ lookup('env', 'ANSIBLE_INVENTORY') }}" 13 | ignore_errors: true 14 | 15 | - name: Test inventories 16 | vars: 17 | inventory_path: "{{ lookup('env', 'ANSIBLE_INVENTORY') }}" 18 | block: 19 | - name: Create temporary file to store backend configuration 20 | tempfile: 21 | suffix: ".hcl" 22 | register: tmpfile 23 | 24 | - name: Generate backend configuration file 25 | template: 26 | src: backend.hcl.j2 27 | dest: "{{ tmpfile.path }}" 28 | 29 | # Simple inventory configuration (default value for search_child_modules is false) 30 | - name: Generate inventory file with backend_config 31 | template: 32 | src: "inventory.yml.j2" 33 | dest: "{{ inventory_path }}" 34 | 35 | - meta: refresh_inventory 36 | 37 | - name: 'assert that host {{ default_hostname }} is defined' 38 | assert: 39 | that: 40 | - default_hostname in hostvars 41 | 42 | - name: 'assert that host {{ child_default_hostname }} is undefined (search_child_modules=false)' 43 | assert: 44 | that: 45 | - child_default_hostname not in hostvars 46 | 47 | - name: Assert that '{{ default_hostname }}' host has required variables 48 | assert: 49 | that: 50 | - item in hostvars[default_hostname] 51 | with_items: "{{ host_variables }}" 52 | 53 | # Inventory with search_child_modules=true 54 | - name: Generate inventory file with 'search_child_modules=true' 55 | template: 56 | src: "inventory_search_child_module.yml.j2" 57 | dest: "{{ inventory_path }}" 58 | 59 | - meta: refresh_inventory 60 | 61 | - name: 'assert that hosts {{ default_hostname }} and {{ child_default_hostname }} are defined' 62 | assert: 63 | that: 64 | - default_hostname in hostvars 65 | - child_default_hostname in hostvars 66 | 67 | - name: 'Assert that hosts {{ default_hostname }}, {{ child_default_hostname }} have variable {{ item }}' 68 | assert: 69 | that: 70 | - item in hostvars[default_hostname] 71 | - item in hostvars[child_default_hostname] 72 | with_items: "{{ host_variables }}" 73 | 74 | # Inventory with backend_config_files 75 | - name: Generate inventory file with backend_config_files 76 | template: 77 | src: "inventory_with_backend_files.yml.j2" 78 | dest: "{{ inventory_path }}" 79 | vars: 80 | backend_config_files: "{{ tmpfile.path }}" 81 | 82 | - meta: refresh_inventory 83 | 84 | - name: 'assert that host {{ default_hostname }} is defined' 85 | assert: 86 | that: 87 | - default_hostname in hostvars 88 | 89 | - name: Assert that '{{ default_hostname }}' host has required variables 90 | assert: 91 | that: 92 | - item in hostvars[default_hostname] 93 | with_items: "{{ host_variables }}" 94 | 95 | # inventory with hostnames 96 | - name: Generate inventory with hostnames 97 | template: 98 | src: inventory_with_hostname.yml.j2 99 | dest: "{{ inventory_path }}" 100 | 101 | - meta: refresh_inventory 102 | 103 | - ansible.builtin.debug: 104 | var: hostvars 105 | 106 | - name: Validate that '{{ default_hostname }}' is not defined 107 | assert: 108 | that: 109 | - tag_hostname in hostvars 110 | - default_hostname not in hostvars 111 | vars: 112 | tag_hostname: "t2.micro-{{ resource_prefix }}-ec2" 113 | 114 | # inventory with compose 115 | - name: Generate inventory with compose 116 | template: 117 | src: inventory_with_compose.yml.j2 118 | dest: "{{ inventory_path }}" 119 | 120 | - meta: refresh_inventory 121 | 122 | - debug: var=hostvars 123 | 124 | - name: Validate variable for host '{{ default_hostname }}' 125 | assert: 126 | that: 127 | - default_hostname in hostvars 128 | - "'ansible_host' in hostvars[default_hostname]" 129 | - hostvars[default_hostname].ansible_host == hostvars[default_hostname].private_ip 130 | 131 | # inventory with constructed 132 | - name: Generate inventory with constructed 133 | template: 134 | src: inventory_with_constructed.yml.j2 135 | dest: "{{ inventory_path }}" 136 | 137 | - meta: refresh_inventory 138 | 139 | - debug: var=groups 140 | 141 | - name: Validate host groups 142 | assert: 143 | that: 144 | - default_hostname in hostvars 145 | - "'tag_Inventory_terraform_state' in groups" 146 | - default_hostname in groups.tag_Inventory_terraform_state 147 | - "'tag_Phase_integration' in groups" 148 | - default_hostname in groups.tag_Phase_integration 149 | - "'state_running' in groups" 150 | - default_hostname in groups.state_running 151 | - "'no_public_ip' in groups" 152 | - default_hostname in groups.no_public_ip 153 | 154 | always: 155 | - name: Delete temporary file 156 | file: 157 | state: absent 158 | path: "{{ tmpfile.path }}" 159 | 160 | - name: Delete inventory file 161 | file: 162 | state: absent 163 | path: "{{ inventory_path }}" -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_aws/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | host_variables: 3 | - ami 4 | - arn 5 | - cpu_core_count 6 | - cpu_options 7 | - disable_api_stop 8 | - disable_api_termination 9 | - id 10 | - instance_state 11 | - instance_type 12 | - key_name 13 | - placement_group 14 | - private_dns 15 | - private_ip 16 | - public_dns 17 | - public_ip 18 | - tags 19 | - user_data 20 | default_hostname: "aws_instance_test" 21 | child_default_hostname: "aws_instance_test_tiny" 22 | child_module_path: "tiny-instance" 23 | bucket_name: "bucket-{{ resource_prefix }}" 24 | resource_name: "ansible-test-state" -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/aliases: -------------------------------------------------------------------------------- 1 | cloud/azure -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/runme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | function cleanup() { 6 | rm -f azurerm.terraform_state.yml azure_credentials.sh 7 | ansible-playbook teardown.yml "$@" 8 | exit 1 9 | } 10 | 11 | trap 'cleanup "${@}"' ERR 12 | 13 | # Create infrastructure 14 | ansible-playbook setup.yml -e "inventory_file_path=azurerm.terraform_state.yml" "$@" 15 | 16 | export ANSIBLE_INVENTORY_ENABLED="cloud.terraform.terraform_state" 17 | 18 | set +x 19 | source azure_credentials.sh 20 | set -x 21 | 22 | ansible-playbook test.yml -i azurerm.terraform_state.yml "$@" 23 | 24 | # Delete infrastructure 25 | rm -f azurerm.terraform_state.yml azure_credentials.sh 26 | ansible-playbook teardown.yml "$@" 27 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/setup.yml: -------------------------------------------------------------------------------- 1 | - name: Create infrastructure 2 | hosts: localhost 3 | gather_facts: false 4 | 5 | vars: 6 | azurerm_client_id: "{{ lookup('env', 'AZURE_CLIENT_ID', default=Undefined) }}" 7 | azurerm_client_secret: "{{ lookup('env', 'AZURE_SECRET', default=Undefined) }}" 8 | azurerm_subscription_id: "{{ lookup('env', 'AZURE_SUBSCRIPTION_ID', default=Undefined) }}" 9 | azurerm_tenant_id: "{{ lookup('env', 'AZURE_TENANT', default=Undefined) }}" 10 | 11 | vars_files: 12 | - vars/main.yml 13 | 14 | environment: 15 | ARM_CLIENT_ID: "{{ lookup('env', 'AZURE_CLIENT_ID', default=Undefined) }}" 16 | ARM_CLIENT_SECRET: "{{ lookup('env', 'AZURE_SECRET', default=Undefined) }}" 17 | ARM_SUBSCRIPTION_ID: "{{ lookup('env', 'AZURE_SUBSCRIPTION_ID', default=Undefined) }}" 18 | ARM_TENANT_ID: "{{ lookup('env', 'AZURE_TENANT', default=Undefined) }}" 19 | 20 | tasks: 21 | # Create Storage account and Container to be used as Backend for Terraform deployment 22 | - name: Create Storage account 23 | azure.azcollection.azure_rm_storageaccount: 24 | resource_group: "{{ resource_group }}" 25 | name: "{{ lookup('ansible.builtin.password', '/dev/null', chars=['ascii_lowercase', 'digits'], length=12) }}" 26 | type: "Standard_LRS" 27 | register: create_storage 28 | retries: 300 29 | delay: 1 30 | until: create_storage is successful 31 | 32 | - name: Set storage account name 33 | set_fact: 34 | test_storage_account_name: "{{ create_storage.state.name }}" 35 | 36 | - name: Create Container 37 | azure.azcollection.azure_rm_storageblob: 38 | resource_group: "{{ resource_group }}" 39 | storage_account_name: "{{ create_storage.state.name }}" 40 | container: "{{ azurerm_backend_container_name }}" 41 | 42 | # Create AzureRM virtual machine using Terraform configuration 43 | - block: 44 | - name: Create temporary dir 45 | ansible.builtin.tempfile: 46 | state: directory 47 | register: terraform_dir 48 | 49 | - name: Generate Terraform configuration 50 | ansible.builtin.template: 51 | src: main.tf.j2 52 | dest: "{{ terraform_dir.path }}/main.tf" 53 | 54 | - name: Create infrastructure using Terraform configuration 55 | cloud.terraform.terraform: 56 | project_path: "{{ terraform_dir.path }}" 57 | force_init: true 58 | variables: 59 | azure_resource_group: "{{ resource_group }}" 60 | always: 61 | - name: Delete temporary directory 62 | ansible.builtin.file: 63 | state: absent 64 | path: "{{ terraform_dir.path }}" 65 | 66 | # Generate inventory file 67 | - name: Generate inventory file 68 | ansible.builtin.template: 69 | src: inventory.yml.j2 70 | dest: "{{ inventory_file_path }}" 71 | 72 | # Generate credentials file 73 | - name: Generate AzureRM credentials file 74 | ansible.builtin.template: 75 | src: azure_credentials.sh.j2 76 | dest: azure_credentials.sh 77 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/teardown.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Delete infrastructure 3 | hosts: localhost 4 | gather_facts: false 5 | 6 | environment: 7 | ARM_CLIENT_ID: "{{ lookup('env', 'AZURE_CLIENT_ID', default=Undefined) }}" 8 | ARM_CLIENT_SECRET: "{{ lookup('env', 'AZURE_SECRET', default=Undefined) }}" 9 | ARM_SUBSCRIPTION_ID: "{{ lookup('env', 'AZURE_SUBSCRIPTION_ID', default=Undefined) }}" 10 | ARM_TENANT_ID: "{{ lookup('env', 'AZURE_TENANT', default=Undefined) }}" 11 | 12 | vars_files: 13 | - vars/main.yml 14 | 15 | tasks: 16 | - name: Read storage account name from resource group 17 | azure.azcollection.azure_rm_storageaccount_info: 18 | resource_group: "{{ resource_group }}" 19 | register: storage_accounts 20 | 21 | - name: Destroy resources using Terraform configuration 22 | when: storage_accounts.storageaccounts | length > 0 23 | block: 24 | - name: Create temporary dir 25 | ansible.builtin.tempfile: 26 | state: directory 27 | register: terraform_dir 28 | 29 | - name: Generate Terraform configuration 30 | ansible.builtin.template: 31 | src: main.tf.j2 32 | dest: "{{ terraform_dir.path }}/main.tf" 33 | vars: 34 | test_storage_account_name: "{{ storage_accounts.storageaccounts.0.name }}" 35 | 36 | - name: Destroy infrastructure using Terraform configuration 37 | cloud.terraform.terraform: 38 | project_path: "{{ terraform_dir.path }}" 39 | force_init: true 40 | variables: 41 | azure_resource_group: "{{ resource_group }}" 42 | state: absent 43 | 44 | - name: Delete Container (force deletion even if it contains blob) 45 | azure.azcollection.azure_rm_storageblob: 46 | resource_group: "{{ resource_group }}" 47 | storage_account_name: "{{ storage_accounts.storageaccounts.0.name }}" 48 | container: "{{ azurerm_backend_container_name }}" 49 | state: absent 50 | force: true 51 | 52 | - name: Delete Storage account 53 | azure.azcollection.azure_rm_storageaccount: 54 | resource_group: "{{ resource_group }}" 55 | name: "{{ storage_accounts.storageaccounts.0.name }}" 56 | state: absent 57 | 58 | always: 59 | - name: Delete temporary directory 60 | ansible.builtin.file: 61 | state: absent 62 | path: "{{ terraform_dir.path }}" 63 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/templates/azure_credentials.sh.j2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export ARM_CLIENT_ID="{{ azurerm_client_id }}" 4 | export ARM_CLIENT_SECRET="{{ azurerm_client_secret }}" 5 | export ARM_SUBSCRIPTION_ID="{{ azurerm_subscription_id }}" 6 | export ARM_TENANT_ID="{{ azurerm_tenant_id }}" -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/templates/inventory.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: azurerm 4 | backend_config: 5 | resource_group_name: {{ resource_group }} 6 | storage_account_name: {{ test_storage_account_name }} 7 | container_name: {{ azurerm_backend_container_name }} 8 | key: {{ azurerm_backend_container_key }} -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/templates/main.tf.j2: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azurerm = { 4 | source = "hashicorp/azurerm" 5 | version = "3.87.0" 6 | } 7 | } 8 | backend "azurerm" { 9 | resource_group_name = "{{ resource_group }}" 10 | storage_account_name = "{{ test_storage_account_name }}" 11 | container_name = "{{ azurerm_backend_container_name }}" 12 | key = "{{ azurerm_backend_container_key }}" 13 | } 14 | } 15 | 16 | variable "azure_resource_group" { 17 | type = string 18 | } 19 | 20 | provider "azurerm" { 21 | skip_provider_registration = true 22 | features {} 23 | } 24 | 25 | data "azurerm_resource_group" "main" { 26 | name = var.azure_resource_group 27 | } 28 | 29 | resource "azurerm_virtual_network" "main" { 30 | name = "test-network" 31 | resource_group_name = data.azurerm_resource_group.main.name 32 | location = data.azurerm_resource_group.main.location 33 | address_space = ["168.10.0.0/16"] 34 | } 35 | 36 | resource "azurerm_subnet" "main" { 37 | name = "test-subnet" 38 | resource_group_name = data.azurerm_resource_group.main.name 39 | virtual_network_name = azurerm_virtual_network.main.name 40 | address_prefixes = ["168.10.1.0/24"] 41 | } 42 | 43 | resource "azurerm_network_interface" "main" { 44 | name = "ansible-test-nic" 45 | location = data.azurerm_resource_group.main.location 46 | resource_group_name = data.azurerm_resource_group.main.name 47 | 48 | ip_configuration { 49 | name = "config1" 50 | subnet_id = azurerm_subnet.main.id 51 | private_ip_address_allocation = "Dynamic" 52 | } 53 | } 54 | 55 | resource "azurerm_virtual_machine" "main" { 56 | name = "ansible-test-vm" 57 | location = data.azurerm_resource_group.main.location 58 | resource_group_name = data.azurerm_resource_group.main.name 59 | network_interface_ids = [azurerm_network_interface.main.id] 60 | vm_size = "Standard_DS1_v2" 61 | 62 | delete_os_disk_on_termination = true 63 | delete_data_disks_on_termination = true 64 | 65 | storage_image_reference { 66 | publisher = "Canonical" 67 | offer = "0001-com-ubuntu-server-jammy" 68 | sku = "22_04-lts" 69 | version = "latest" 70 | } 71 | storage_os_disk { 72 | name = "myosdisk1" 73 | caching = "ReadWrite" 74 | create_option = "FromImage" 75 | managed_disk_type = "Standard_LRS" 76 | } 77 | os_profile { 78 | computer_name = "hostname" 79 | admin_username = "ansible" 80 | admin_password = "testing123!" 81 | } 82 | os_profile_linux_config { 83 | disable_password_authentication = false 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | gather_facts: false 4 | 5 | tasks: 6 | - name: Ensure host is defined with expected variables 7 | assert: 8 | that: 9 | - '"azurerm_virtual_machine_main" in hostvars' 10 | - 'item in hostvars["azurerm_virtual_machine_main"]' 11 | with_items: 12 | - availability_set_id 13 | - boot_diagnostics 14 | - delete_data_disks_on_termination 15 | - delete_os_disk_on_termination 16 | - id 17 | - location 18 | - os_profile 19 | - os_profile_linux_config 20 | - storage_image_reference 21 | - storage_os_disk 22 | - vm_size 23 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_azurerm/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | azurerm_backend_container_name: "tfstate" 3 | azurerm_backend_container_key: "terraform.tfstate" -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/aliases: -------------------------------------------------------------------------------- 1 | cloud/gcp -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/runme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | function cleanup() { 6 | rm -f google.terraform_state.yml gcp_credentials.sh 7 | ansible-playbook teardown.yml "$@" 8 | exit 1 9 | } 10 | 11 | trap 'cleanup "${@}"' ERR 12 | 13 | # Create infrastructure 14 | ansible-playbook setup.yml -e "inventory_file_path=google.terraform_state.yml" "$@" 15 | 16 | # export ANSIBLE_INVENTORY_ENABLED="cloud.terraform.terraform_state" 17 | set +x 18 | source gcp_credentials.sh 19 | set -x 20 | 21 | ansible-playbook test.yml -i google.terraform_state.yml "$@" 22 | 23 | # Delete infrastructure 24 | rm -f google.terraform_state.yml gcp_credentials.sh 25 | ansible-playbook teardown.yml "$@" 26 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/setup.yml: -------------------------------------------------------------------------------- 1 | - name: Create infrastructure 2 | hosts: localhost 3 | gather_facts: false 4 | 5 | vars_files: 6 | - vars/main.yml 7 | 8 | environment: 9 | GOOGLE_PROJECT: "{{ gcp_project }}" 10 | GOOGLE_REGION: "us-east-1" 11 | GOOGLE_CREDENTIALS: "{{ gcp_cred_file }}" 12 | 13 | tasks: 14 | # Create The storage bucket acting as Terraform remote backend 15 | - name: Create a Storage bucket 16 | google.cloud.gcp_storage_bucket: 17 | name: "{{ gcp_storage_bucket_name }}" 18 | storage_class: STANDARD 19 | project: "{{ gcp_project }}" 20 | auth_kind: "{{ gcp_cred_kind }}" 21 | service_account_file: "{{ gcp_cred_file }}" 22 | 23 | # Create GCP instance 24 | - block: 25 | - name: Create temporary dir 26 | ansible.builtin.tempfile: 27 | state: directory 28 | register: terraform_dir 29 | 30 | - name: Generate Terraform configuration 31 | ansible.builtin.template: 32 | src: main.tf.j2 33 | dest: "{{ terraform_dir.path }}/main.tf" 34 | 35 | - name: Create infrastructure using Terraform configuration 36 | cloud.terraform.terraform: 37 | project_path: "{{ terraform_dir.path }}" 38 | force_init: true 39 | variables: 40 | instance_name: "{{ gcp_instance_name }}" 41 | 42 | always: 43 | - name: Delete temporary directory 44 | ansible.builtin.file: 45 | state: absent 46 | path: "{{ terraform_dir.path }}" 47 | 48 | # Generate inventory file 49 | - name: Generate inventory file 50 | ansible.builtin.template: 51 | src: inventory.yml.j2 52 | dest: "{{ inventory_file_path }}" 53 | 54 | # Generate credentials file 55 | - name: Generate GCP credentials file 56 | ansible.builtin.template: 57 | src: gcp_credentials.sh.j2 58 | dest: gcp_credentials.sh 59 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/teardown.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Delete infrastructure 3 | hosts: localhost 4 | gather_facts: false 5 | 6 | vars_files: 7 | - vars/main.yml 8 | 9 | environment: 10 | GOOGLE_PROJECT: "{{ gcp_project }}" 11 | GOOGLE_REGION: "us-east-1" 12 | GOOGLE_CREDENTIALS: "{{ gcp_cred_file }}" 13 | 14 | tasks: 15 | # Delete GCP instance 16 | - block: 17 | - name: Create temporary dir 18 | ansible.builtin.tempfile: 19 | state: directory 20 | register: terraform_dir 21 | 22 | - name: Generate Terraform configuration 23 | ansible.builtin.template: 24 | src: main.tf.j2 25 | dest: "{{ terraform_dir.path }}/main.tf" 26 | 27 | - name: Delete infrastructure using Terraform configuration 28 | cloud.terraform.terraform: 29 | project_path: "{{ terraform_dir.path }}" 30 | force_init: true 31 | state: absent 32 | variables: 33 | instance_name: "{{ gcp_instance_name }}" 34 | always: 35 | - name: Delete temporary directory 36 | ansible.builtin.file: 37 | state: absent 38 | path: "{{ terraform_dir.path }}" 39 | 40 | - name: Delete object from bucket 41 | google.cloud.gcp_storage_object: 42 | bucket: "{{ gcp_storage_bucket_name }}" 43 | src: "{{ gcp_storage_bucket_prefix }}/default.tfstate" 44 | action: delete 45 | project: "{{ gcp_project }}" 46 | auth_kind: "{{ gcp_cred_kind }}" 47 | service_account_file: "{{ gcp_cred_file }}" 48 | ignore_errors: true 49 | 50 | - name: Delete a Storage bucket 51 | google.cloud.gcp_storage_bucket: 52 | name: "{{ gcp_storage_bucket_name }}" 53 | state: absent 54 | project: "{{ gcp_project }}" 55 | auth_kind: "{{ gcp_cred_kind }}" 56 | service_account_file: "{{ gcp_cred_file }}" 57 | ignore_errors: true 58 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/templates/gcp_credentials.sh.j2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export GOOGLE_PROJECT="{{ gcp_project }}" 4 | export GOOGLE_REGION="us-east-1" 5 | export GOOGLE_CREDENTIALS="{{ gcp_cred_file }}" -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/templates/inventory.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_state 3 | backend_type: gcs 4 | backend_config: 5 | bucket: {{ gcp_storage_bucket_name }} 6 | prefix: {{ gcp_storage_bucket_prefix }} -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/templates/main.tf.j2: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "5.11.0" 6 | } 7 | } 8 | backend "gcs" { 9 | bucket = "{{ gcp_storage_bucket_name }}" 10 | prefix = "{{ gcp_storage_bucket_prefix }}" 11 | } 12 | } 13 | 14 | provider "google" { 15 | } 16 | 17 | variable "instance_name" { 18 | type = string 19 | } 20 | 21 | resource "google_compute_instance" "default" { 22 | name = var.instance_name 23 | machine_type = "n2-standard-2" 24 | zone = "us-east1-c" 25 | 26 | boot_disk { 27 | initialize_params { 28 | image = "debian-cloud/debian-11" 29 | labels = { 30 | my_label = "value" 31 | } 32 | } 33 | } 34 | 35 | scratch_disk { 36 | interface = "NVME" 37 | } 38 | 39 | network_interface { 40 | network = "default" 41 | access_config { 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | gather_facts: false 4 | 5 | tasks: 6 | - name: Ensure host is defined with expected variables 7 | assert: 8 | that: 9 | - '"google_compute_instance_default" in hostvars' 10 | - 'item in hostvars["google_compute_instance_default"]' 11 | with_items: 12 | - advanced_machine_features 13 | - allow_stopping_for_update 14 | - attached_disk 15 | - boot_disk 16 | - can_ip_forward 17 | - confidential_instance_config 18 | - deletion_protection 19 | - id 20 | - machine_type 21 | - network_interface 22 | - scheduling 23 | - scratch_disk 24 | -------------------------------------------------------------------------------- /tests/integration/targets/inventory_terraform_state_google/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | gcp_storage_bucket_name: "{{ resource_prefix }}-bucket" 3 | gcp_storage_bucket_prefix: "terraform_state" 4 | gcp_instance_name: "{{ resource_prefix }}-instance" 5 | -------------------------------------------------------------------------------- /tests/integration/targets/list_vars_passthrough/files/main.tf: -------------------------------------------------------------------------------- 1 | variable "vms" { 2 | type = list(string) 3 | } 4 | 5 | resource "null_resource" "debug" { 6 | provisioner "local-exec" { 7 | interpreter = ["/bin/bash", "-c"] 8 | command = "echo '${jsonencode(var.vms)}' > ${path.module}/out.txt" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/targets/list_vars_passthrough/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | test_basedir: "{{ test_basedir | default(output_dir) }}" 3 | 4 | - name: Copy terraform files to work space 5 | ansible.builtin.copy: 6 | src: "{{ item }}" 7 | dest: "{{ test_basedir }}/{{ item }}" 8 | loop: 9 | - main.tf 10 | 11 | - &stat 12 | name: Run stat on the result file 13 | stat: 14 | path: "{{ test_basedir }}/out.txt" 15 | register: stat 16 | - name: The test file must not exist 17 | assert: 18 | that: not stat.stat.exists 19 | 20 | - name: Test with complex vars disabled 21 | cloud.terraform.terraform: 22 | project_path: "{{ test_basedir }}" 23 | state: present 24 | force_init: true 25 | variables: 26 | vms: "{{ vms }}" 27 | complex_vars: false 28 | vars: 29 | vms: 30 | - "asdf" 31 | - "qwer" 32 | register: terraform_output 33 | ignore_errors: true 34 | 35 | - *stat 36 | - name: The test file must not exist after a failure 37 | assert: 38 | that: 39 | - not stat.stat.exists 40 | - terraform_output is failed 41 | 42 | - name: Test with complex vars enabled 43 | cloud.terraform.terraform: 44 | project_path: "{{ test_basedir }}" 45 | state: present 46 | force_init: true 47 | variables: 48 | vms: "{{ vms }}" 49 | complex_vars: true 50 | vars: 51 | vms: 52 | - "asdf" 53 | - "qwer" 54 | register: terraform_output 55 | 56 | - *stat 57 | - name: The test file must exist 58 | assert: 59 | that: 60 | - stat.stat.exists 61 | 62 | - ansible.builtin.slurp: 63 | src: "{{ test_basedir }}/out.txt" 64 | register: slurp 65 | - debug: 66 | msg: ">{{ slurp.content | b64decode }}<" 67 | - name: Verify file contents 68 | assert: 69 | that: 70 | - (slurp.content | b64decode) == '["asdf","qwer"]\n' 71 | -------------------------------------------------------------------------------- /tests/integration/targets/local/files/write_file.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | provider "local" { 11 | # Configuration options (I have none :_) 12 | } 13 | 14 | resource "local_file" "foo" { 15 | content = "This file was written by terraform!" 16 | filename = "${path.module}/terraform_test.txt" 17 | } 18 | -------------------------------------------------------------------------------- /tests/integration/targets/local/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - set_fact: 7 | test_basedir: "{{ test_basedir | default(output_dir) }}" 8 | 9 | - name: Copy terraform files to work space 10 | ansible.builtin.copy: 11 | src: "{{ item }}" 12 | dest: "{{ test_basedir }}/{{ item }}" 13 | loop: 14 | - write_file.tf 15 | 16 | - &stat 17 | name: Run stat on the result file 18 | stat: 19 | path: "{{ test_basedir }}/terraform_test.txt" 20 | register: stat 21 | - name: The test file must not exist 22 | assert: 23 | that: not stat.stat.exists 24 | 25 | - name: Terraform in present check mode 26 | cloud.terraform.terraform: 27 | project_path: "{{ test_basedir }}" 28 | state: present 29 | force_init: true 30 | register: terraform_result 31 | check_mode: true 32 | - assert: 33 | that: 34 | - terraform_result is not failed 35 | - terraform_result is changed 36 | - *stat 37 | - name: The test file must not exist 38 | assert: 39 | that: not stat.stat.exists 40 | 41 | - name: Terraform in present non-check mode 42 | cloud.terraform.terraform: 43 | project_path: "{{ test_basedir }}" 44 | state: present 45 | force_init: true 46 | register: terraform_result 47 | check_mode: false 48 | - assert: 49 | that: 50 | - terraform_result is changed 51 | - *stat 52 | - name: The test file must exist 53 | assert: 54 | that: stat.stat.exists 55 | 56 | - name: Terraform in present non-check mode (idempotency) 57 | cloud.terraform.terraform: 58 | project_path: "{{ test_basedir }}" 59 | state: present 60 | register: terraform_result 61 | check_mode: false 62 | - assert: 63 | that: 64 | - terraform_result is not changed 65 | - *stat 66 | - name: The test file must exist 67 | assert: 68 | that: stat.stat.exists 69 | 70 | - name: Terraform in absent check mode 71 | cloud.terraform.terraform: 72 | project_path: "{{ test_basedir }}" 73 | state: absent 74 | register: terraform_result 75 | check_mode: true 76 | - assert: 77 | that: 78 | - terraform_result is changed 79 | - *stat 80 | - name: The test file must exist 81 | assert: 82 | that: stat.stat.exists 83 | 84 | - name: Terraform in absent non-check mode 85 | cloud.terraform.terraform: 86 | project_path: "{{ test_basedir }}" 87 | state: absent 88 | register: terraform_result 89 | check_mode: false 90 | - assert: 91 | that: 92 | - terraform_result is changed 93 | - *stat 94 | - name: The test file must not exist 95 | assert: 96 | that: not stat.stat.exists 97 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup/outputs.tf: -------------------------------------------------------------------------------- 1 | variable "source_input" { 2 | type = string 3 | } 4 | 5 | output "source_output" { 6 | value = var.source_input 7 | } 8 | 9 | output "my_output1" { 10 | value = "value1" 11 | } 12 | 13 | output "my_output2" { 14 | value = "value2" 15 | } 16 | 17 | output "my_output3" { 18 | value = "value3" 19 | } 20 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup/runme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | ( 6 | mkdir -p subdir/ 7 | cp outputs.tf subdir/ 8 | cd subdir/ 9 | terraform init 10 | terraform apply -var source_input=hello_project -auto-approve 11 | terraform apply -var source_input=hello_custom -auto-approve -state mycustomstate.tfstate 12 | ) 13 | 14 | ansible-playbook test_outputs.yml 15 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup/test_outputs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | - name: Test 6 | hosts: localhost 7 | tasks: 8 | - name: Test project path 9 | ansible.builtin.assert: 10 | that: 11 | - lookup('cloud.terraform.tf_output', 'my_output1', 'my_output2', project_path="subdir/") == "value1,value2" 12 | - lookup('cloud.terraform.tf_output', 'source_output', project_path="subdir/") == "hello_project" 13 | - lookup('cloud.terraform.tf_output', project_path="subdir/").source_output.value == "hello_project" 14 | 15 | - name: Test state file 16 | ansible.builtin.assert: 17 | that: 18 | - lookup('cloud.terraform.tf_output', 'my_output1', 'my_output2', state_file="subdir/mycustomstate.tfstate") == "value1,value2" 19 | - lookup('cloud.terraform.tf_output', 'source_output', state_file="subdir/mycustomstate.tfstate") == "hello_custom" 20 | - lookup('cloud.terraform.tf_output', state_file="subdir/mycustomstate.tfstate").source_output.value == "hello_custom" 21 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_cwd/outputs.tf: -------------------------------------------------------------------------------- 1 | output "my_output1" { 2 | value = "value1" 3 | } 4 | 5 | output "my_output2" { 6 | value = "value2" 7 | } 8 | 9 | output "my_output3" { 10 | value = "value3" 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_cwd/runme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | terraform init 6 | terraform apply -auto-approve 7 | 8 | ansible-playbook test_outputs.yml 9 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_cwd/test_outputs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | - name: Test 6 | hosts: localhost 7 | tasks: 8 | - ansible.builtin.assert: 9 | that: 10 | - lookup('cloud.terraform.tf_output', 'my_output3') == "value3" 11 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_datadir/outputs.tf: -------------------------------------------------------------------------------- 1 | output "my_output1" { 2 | value = "value1" 3 | } 4 | 5 | output "my_output2" { 6 | value = "value2" 7 | } 8 | 9 | output "my_output3" { 10 | value = "value3" 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_datadir/runme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ( 4 | set -eux 5 | mkdir -p terraform_init 6 | cd terraform_init 7 | terraform init 8 | ) 9 | 10 | export TF_DATA_DIR="$PWD/terraform_init/.terraform" 11 | env | grep TF_ 12 | 13 | # this uses TF_DATA_DIR 14 | terraform apply -auto-approve 15 | 16 | ansible-playbook test_outputs.yml 17 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_datadir/test_outputs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | - name: Test 6 | hosts: localhost 7 | tasks: 8 | - ansible.builtin.assert: 9 | that: 10 | - lookup('cloud.terraform.tf_output', 'my_output3') == "value3" 11 | -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_workspace/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | test_workspaces: 3 | - dev 4 | - qa 5 | - prod -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_workspace/files/outputs.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | variable "workspace" { 11 | type = string 12 | } 13 | 14 | output "my_workspace" { 15 | value = "${var.workspace}" 16 | } -------------------------------------------------------------------------------- /tests/integration/targets/output_lookup_workspace/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - block: 3 | - name: Create temporary directory to work in 4 | ansible.builtin.tempfile: 5 | state: directory 6 | suffix: .tf 7 | register: tf_dir 8 | 9 | - name: Copy terraform files 10 | ansible.builtin.copy: 11 | src: outputs.tf 12 | dest: "{{ tf_dir.path }}" 13 | 14 | - name: Apply terraform projects 15 | cloud.terraform.terraform: 16 | force_init: true 17 | workspace: '{{ item }}' 18 | project_path: '{{ tf_dir.path }}' 19 | variables: 20 | workspace: "{{ item }}" 21 | with_items: "{{ test_workspaces }}" 22 | 23 | - name: Ensure module returned values as expected 24 | ansible.builtin.assert: 25 | that: 26 | - item == tf_outputs.my_workspace.value 27 | with_items: "{{ test_workspaces }}" 28 | vars: 29 | tf_outputs: "{{ lookup('cloud.terraform.tf_output', project_path=tf_dir.path, workspace=item) }}" 30 | 31 | always: 32 | - name: Delete temporary directory 33 | ansible.builtin.file: 34 | state: absent 35 | path: "{{ tf_dir.path }}" -------------------------------------------------------------------------------- /tests/integration/targets/plan_stash/files/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | random = { 4 | source = "hashicorp/random" 5 | version = "3.6.0" 6 | } 7 | } 8 | } 9 | 10 | resource "random_string" "random" { 11 | length = 16 12 | special = true 13 | } -------------------------------------------------------------------------------- /tests/integration/targets/plan_stash/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Validate plan_stash module 2 | ansible.builtin.include_tasks: 'tasks/{{ item }}.yml' 3 | with_items: 4 | - validate_args 5 | - run 6 | -------------------------------------------------------------------------------- /tests/integration/targets/plan_stash/tasks/run.yml: -------------------------------------------------------------------------------- 1 | - name: Create temporary directory to work in 2 | ansible.builtin.tempfile: 3 | state: directory 4 | suffix: .tfplan 5 | register: test_dir 6 | 7 | - name: Run tests 8 | block: 9 | - name: Copy terraform configuration into working directory 10 | copy: 11 | src: main.tf 12 | dest: "{{ test_dir.path }}" 13 | 14 | - name: Run terraform plan 15 | cloud.terraform.terraform: 16 | force_init: true 17 | project_path: "{{ test_dir.path }}" 18 | plan_file: "{{ test_dir.path }}/terraform.tfplan" 19 | check_mode: true 20 | 21 | - set_fact: 22 | encoded_data: "{{ lookup('file', test_dir.path + '/terraform.tfplan') | b64encode }}" 23 | 24 | # Stash the terraform plan file 25 | - name: Save the terraform plan into stats 26 | cloud.terraform.plan_stash: 27 | path: "{{ test_dir.path }}/terraform.tfplan" 28 | per_host: true 29 | register: plan_stash 30 | 31 | - name: Ensure terraform plan file has been saved as expected 32 | ansible.builtin.assert: 33 | that: 34 | - plan_stash is not changed 35 | - '"ansible_stats" in plan_stash' 36 | - '"data" in plan_stash.ansible_stats' 37 | - '"terraform_plan" in plan_stash.ansible_stats.data' 38 | - plan_stash.ansible_stats.data.terraform_plan == encoded_data 39 | - '"per_host" in plan_stash.ansible_stats' 40 | - plan_stash.ansible_stats.per_host 41 | 42 | - name: Save the terraform plan into a custom variable name 43 | cloud.terraform.plan_stash: 44 | path: "{{ test_dir.path }}/terraform.tfplan" 45 | var_name: "terraform_plan_custom_variable" 46 | register: plan_stash 47 | 48 | - name: Ensure terraform plan file has been saved as expected 49 | ansible.builtin.assert: 50 | that: 51 | - plan_stash is not changed 52 | - '"ansible_stats" in plan_stash' 53 | - '"data" in plan_stash.ansible_stats' 54 | - '"terraform_plan_custom_variable" in plan_stash.ansible_stats.data' 55 | - plan_stash.ansible_stats.data.terraform_plan_custom_variable == encoded_data 56 | - '"per_host" in plan_stash.ansible_stats' 57 | - not plan_stash.ansible_stats.per_host 58 | 59 | - name: Save terraform base64-encoded data into variable 60 | set_fact: 61 | stashed_data: "{{ plan_stash.ansible_stats.data.terraform_plan_custom_variable }}" 62 | 63 | # Load the terraform plan file 64 | - name: Load the terraform plan (check_mode=true) 65 | cloud.terraform.plan_stash: 66 | path: "{{ test_dir.path }}/load.tfplan" 67 | var_name: stashed_data 68 | state: load 69 | register: load_plan_check_mode 70 | check_mode: true 71 | 72 | - name: Stat the terraform plan file 73 | stat: 74 | path: "{{ test_dir.path }}/load.tfplan" 75 | register: stat_tf 76 | 77 | - name: Ensure the module reported changed but the file was not created 78 | assert: 79 | that: 80 | - load_plan_check_mode is changed 81 | - not stat_tf.stat.exists 82 | 83 | - name: Load the terraform plan using var_name 84 | cloud.terraform.plan_stash: 85 | path: "{{ test_dir.path }}/load.tfplan" 86 | var_name: stashed_data 87 | state: load 88 | register: load_plan 89 | 90 | - name: Ensure the terraform plan file has been loaded with the original content 91 | assert: 92 | that: 93 | - load_plan is changed 94 | - lookup('file', test_dir.path + '/load.tfplan') == lookup('file', test_dir.path + '/terraform.tfplan') 95 | 96 | - name: Load the terraform plan using var_name once again (idempotency) 97 | cloud.terraform.plan_stash: 98 | path: "{{ test_dir.path }}/load.tfplan" 99 | var_name: stashed_data 100 | state: load 101 | register: load_idempotency 102 | 103 | - name: Ensure result is not changed 104 | assert: 105 | that: 106 | - load_idempotency is not changed 107 | - lookup('file', test_dir.path + '/load.tfplan') == lookup('file', test_dir.path + '/terraform.tfplan') 108 | 109 | - name: Load the terraform plan using 'binary_data' parameter 110 | cloud.terraform.plan_stash: 111 | path: "{{ test_dir.path }}/load2.tfplan" 112 | binary_data: "{{ stashed_data }}" 113 | state: load 114 | register: load_using_binary 115 | 116 | - name: Ensure the terraform plan file has been loaded with the original content 117 | assert: 118 | that: 119 | - load_using_binary is changed 120 | - lookup('file', test_dir.path + '/load2.tfplan') == lookup('file', test_dir.path + '/terraform.tfplan') 121 | 122 | - name: Load the terraform plan using 'binary_data' parameter once again (idempotency) 123 | cloud.terraform.plan_stash: 124 | path: "{{ test_dir.path }}/load2.tfplan" 125 | binary_data: "{{ stashed_data }}" 126 | state: load 127 | register: load_using_binary_idempotency 128 | 129 | - name: Ensure result is not changed 130 | assert: 131 | that: 132 | - load_using_binary_idempotency is not changed 133 | - lookup('file', test_dir.path + '/load2.tfplan') == lookup('file', test_dir.path + '/terraform.tfplan') 134 | 135 | always: 136 | - name: Delete temporary directory 137 | ansible.builtin.file: 138 | path: "{{ test_dir.path }}" 139 | state: absent -------------------------------------------------------------------------------- /tests/integration/targets/plan_stash/tasks/validate_args.yml: -------------------------------------------------------------------------------- 1 | # Ensure the 'path' argument is required 2 | - name: Run module without path argument 3 | cloud.terraform.plan_stash: 4 | register: missing_path 5 | ignore_errors: true 6 | 7 | - name: Assert that module failed when path is not specified 8 | assert: 9 | that: 10 | - missing_path is failed 11 | - 'missing_path.msg == "missing required arguments: path"' 12 | 13 | # Ensure the module failed when 'var_name' is not a valid variable name 14 | - name: Run module with invalid variable name 15 | cloud.terraform.plan_stash: 16 | path: path_to_plan_file 17 | var_name: .testing_plan 18 | register: invalid_var_name 19 | ignore_errors: true 20 | 21 | - name: Ensure the module failed with invalid variable name 22 | assert: 23 | that: 24 | - invalid_var_name is failed 25 | - 'invalid_var_name.msg == error_message' 26 | vars: 27 | error_message: "The variable name '.testing_plan' is not valid. Variables must start with a letter or underscore character, and contain only letters, numbers and underscores." 28 | 29 | # Validate that module failed with both 'binary_data' and 'var_name' are specified with state=load 30 | - name: Try to run module with both 'binary_data' and 'var_name' and state==load 31 | cloud.terraform.plan_stash: 32 | path: plan_to_plan_file 33 | var_name: terraform_plan 34 | binary_data: "" 35 | state: load 36 | ignore_errors: true 37 | register: validate_load 38 | 39 | - name: Ensure module failed with proper message 40 | assert: 41 | that: 42 | - validate_load is failed 43 | - 'validate_load.msg == error_message' 44 | vars: 45 | error_message: "You cannot specify both 'var_name' and 'binary_data' to load the terraform plan file." 46 | 47 | # Try to load terraform plan using undefined variable 48 | - name: Load terraform plan from undefined variable 49 | cloud.terraform.plan_stash: 50 | path: plan_to_plan_file 51 | var_name: this_ansible_variable_is_undefined 52 | state: load 53 | ignore_errors: true 54 | register: undef_variable 55 | 56 | - name: Ensure module failed with proper message 57 | assert: 58 | that: 59 | - undef_variable is failed 60 | - 'undef_variable.msg == "No variable found with this name: this_ansible_variable_is_undefined"' 61 | -------------------------------------------------------------------------------- /tests/integration/targets/provider_upgrade/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | # This block checks and registers Terraform version of the binary found in path. 7 | 8 | - name: Loop over provider upgrade test tasks 9 | ansible.builtin.include_tasks: test_provider_upgrade.yml 10 | vars: 11 | tf_provider: "{{ terraform_provider_versions[provider_index] }}" 12 | loop: "{{ terraform_provider_versions }}" 13 | loop_control: 14 | index_var: provider_index 15 | 16 | - name: Cleanup terraform project directory 17 | ansible.builtin.file: 18 | path: "{{ terraform_project_dir }}" 19 | state: absent 20 | -------------------------------------------------------------------------------- /tests/integration/targets/provider_upgrade/tasks/test_provider_upgrade.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - name: Create terraform project directory (provider upgrade) 7 | file: 8 | path: "{{ terraform_project_dir }}/{{ item['name'] }}" 9 | state: directory 10 | mode: 0755 11 | loop: "{{ terraform_provider_versions }}" 12 | loop_control: 13 | index_var: provider_index 14 | 15 | - name: Output terraform provider test project 16 | ansible.builtin.template: 17 | src: main.tf.j2 18 | dest: "{{ terraform_project_dir }}/{{ tf_provider['name'] }}/main.tf" 19 | force: true 20 | register: terraform_provider_hcl 21 | 22 | # The purpose of this task is to init terraform multiple times with different provider module 23 | # versions, so that we can verify that provider upgrades during init work as intended. 24 | 25 | - name: Init Terraform configuration with pinned provider version 26 | cloud.terraform.terraform: 27 | project_path: "{{ terraform_provider_hcl.dest | dirname }}" 28 | force_init: true 29 | provider_upgrade: "{{ terraform_provider_upgrade }}" 30 | state: present 31 | register: terraform_init_result 32 | 33 | - assert: 34 | that: terraform_init_result is not failed 35 | -------------------------------------------------------------------------------- /tests/integration/targets/provider_upgrade/templates/main.tf.j2: -------------------------------------------------------------------------------- 1 | {# 2 | Copyright (c) Ansible Project 3 | GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | SPDX-License-Identifier: GPL-3.0-or-later 5 | #} 6 | terraform { 7 | required_providers { 8 | {{ tf_provider['name'] }} = { 9 | source = "{{ tf_provider['source'] }}" 10 | version = "{{ tf_provider['version'] }}" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/integration/targets/provider_upgrade/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | # Directory where Terraform tests will be created 7 | terraform_project_dir: "/tmp/tf_provider_test" 8 | 9 | # Controls whether terraform init will use the `-upgrade` flag 10 | terraform_provider_upgrade: true 11 | 12 | # list of dicts containing Terraform providers that will be tested 13 | # The null provider is a good candidate, as it's small and has no external dependencies 14 | terraform_provider_versions: 15 | - name: "null" 16 | source: "hashicorp/null" 17 | version: ">=2.0.0, < 3.0.0" 18 | - name: "null" 19 | source: "hashicorp/null" 20 | version: ">=3.0.0" 21 | -------------------------------------------------------------------------------- /tests/integration/targets/state_planned/files/write_file.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | provider "local" { 11 | # Configuration options (I have none :_) 12 | } 13 | 14 | resource "local_file" "foo" { 15 | content = "This file was written by terraform!" 16 | filename = "${path.module}/terraform_test.txt" 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tests/integration/targets/state_planned/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - set_fact: 7 | test_basedir: "{{ test_basedir | default(output_dir) }}" 8 | 9 | - name: Copy terraform files to work space 10 | ansible.builtin.copy: 11 | src: "{{ item }}" 12 | dest: "{{ test_basedir }}/{{ item }}" 13 | loop: 14 | - write_file.tf 15 | 16 | - &stat_result_file 17 | name: Run stat on the result file 18 | stat: 19 | path: "{{ test_basedir }}/terraform_test.txt" 20 | register: stat 21 | - name: The test file must not exist 22 | assert: 23 | that: not stat.stat.exists 24 | 25 | - &stat_plan_file 26 | name: Run stat on the result file 27 | stat: 28 | path: "{{ test_basedir }}/my-first-plan.tfplan" 29 | register: stat 30 | - name: The plan file must not exist 31 | assert: 32 | that: not stat.stat.exists 33 | 34 | - name: Terraform in planned mode 35 | cloud.terraform.terraform: 36 | project_path: "{{ test_basedir }}" 37 | state: planned 38 | plan_file: "my-first-plan.tfplan" 39 | force_init: true 40 | register: terraform_result 41 | check_mode: true 42 | - assert: 43 | that: 44 | - terraform_result is not failed 45 | - terraform_result is changed 46 | 47 | - *stat_result_file 48 | - name: The test file must not exist 49 | assert: 50 | that: not stat.stat.exists 51 | 52 | - *stat_plan_file 53 | - name: The plan file must exist 54 | assert: 55 | that: stat.stat.exists 56 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_diff/files/secret.tfvars: -------------------------------------------------------------------------------- 1 | my_output = "sensitive_content" 2 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_diff/files/write_file.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | provider "local" { 11 | # Configuration options (I have none :_) 12 | } 13 | 14 | resource "local_sensitive_file" "sensitive_foo" { 15 | content = "sensitive_content" 16 | filename = "${path.module}/not_sensitive_file_name.txt" 17 | } 18 | 19 | output "my_output" { 20 | value = var.my_output 21 | } 22 | 23 | variable "my_output" { 24 | type = string 25 | } 26 | 27 | output "my_another_output" { 28 | value = "not_sensitive_value" 29 | } 30 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_diff/files/write_file_updated.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | provider "local" { 11 | # Configuration options (I have none :_) 12 | } 13 | 14 | resource "local_sensitive_file" "sensitive_foo" { 15 | content = "new_sensitive_content" 16 | filename = sensitive("${path.module}/new_sensitive_file_name.txt") 17 | } 18 | 19 | output "my_output" { 20 | value = var.my_output 21 | sensitive = true 22 | } 23 | 24 | variable "my_output" { 25 | type = string 26 | } 27 | 28 | output "my_another_output" { 29 | value = "sensitive_value" 30 | sensitive = true 31 | } 32 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_diff/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - set_fact: 7 | test_basedir: "{{ test_basedir | default(output_dir) }}" 8 | 9 | - name: Copy terraform files to work space 10 | ansible.builtin.copy: 11 | src: "{{ item }}" 12 | dest: "{{ test_basedir }}/{{ item }}" 13 | loop: 14 | - write_file.tf 15 | - secret.tfvars 16 | 17 | - name: Terraform init and apply 18 | cloud.terraform.terraform: &init_and_apply 19 | project_path: "{{ test_basedir }}" 20 | state: present 21 | variables_files: 22 | - secret.tfvars 23 | force_init: true 24 | register: terraform_result 25 | - assert: 26 | that: 27 | - terraform_result is not failed 28 | - terraform_result is changed 29 | 30 | - name: Terraform init and apply - idempotence 31 | cloud.terraform.terraform: *init_and_apply 32 | register: terraform_result 33 | - assert: 34 | that: 35 | - terraform_result is not failed 36 | - terraform_result is not changed 37 | 38 | - name: Copy updated terraform module 39 | ansible.builtin.copy: 40 | src: write_file_updated.tf 41 | dest: "{{ test_basedir }}/write_file.tf" 42 | 43 | - name: Terraform plan 44 | cloud.terraform.terraform: 45 | project_path: "{{ test_basedir }}" 46 | state: present 47 | variables_files: 48 | - secret.tfvars 49 | register: terraform_result 50 | check_mode: true 51 | - assert: 52 | that: 53 | - terraform_result is not failed 54 | - terraform_result is changed 55 | - terraform_result.diff.before["values"].outputs.my_output.value == "sensitive_content" 56 | - not terraform_result.diff.after["values"].outputs.my_output.value 57 | - terraform_result.diff.before["values"].outputs.my_another_output.value == "not_sensitive_value" 58 | - not terraform_result.diff.after["values"].outputs.my_another_output.value 59 | - terraform_result.diff.before["values"].root_module.resources[0]["values"].filename == "./not_sensitive_file_name.txt" 60 | - not terraform_result.diff.after["values"].root_module.resources[0]["values"].filename 61 | - not terraform_result.diff.before["values"].root_module.resources[0]["values"].content 62 | - not terraform_result.diff.after["values"].root_module.resources[0]["values"].content 63 | 64 | - name: Terraform apply updates 65 | cloud.terraform.terraform: 66 | project_path: "{{ test_basedir }}" 67 | state: present 68 | variables_files: 69 | - secret.tfvars 70 | register: terraform_result 71 | - assert: 72 | that: 73 | - terraform_result is not failed 74 | - terraform_result is changed 75 | - terraform_result.diff.before["values"].outputs.my_output.value == "sensitive_content" 76 | - not terraform_result.diff.after["values"].outputs.my_output.value 77 | - terraform_result.diff.before["values"].outputs.my_another_output.value == "not_sensitive_value" 78 | - not terraform_result.diff.after["values"].outputs.my_another_output.value 79 | - terraform_result.diff.before["values"].root_module.resources[0]["values"].filename == "./not_sensitive_file_name.txt" 80 | - not terraform_result.diff.after["values"].root_module.resources[0]["values"].filename 81 | - not terraform_result.diff.before["values"].root_module.resources[0]["values"].content 82 | - not terraform_result.diff.after["values"].root_module.resources[0]["values"].content 83 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_output/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | test_workspaces: 3 | - dev 4 | - uat 5 | - qa 6 | - prod -------------------------------------------------------------------------------- /tests/integration/targets/terraform_output/files/outputs.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | output "my_output" { 11 | value = "file generated" 12 | } 13 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_output/files/outputs_workspace.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | variable "workspace" { 11 | type = string 12 | } 13 | 14 | output "my_workspace" { 15 | value = "${var.workspace}" 16 | } -------------------------------------------------------------------------------- /tests/integration/targets/terraform_output/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) Ansible Project 3 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | - set_fact: 7 | test_basedir: "{{ test_basedir | default(output_dir) }}" 8 | 9 | - name: Copy terraform files to work space 10 | ansible.builtin.copy: 11 | src: "{{ item }}" 12 | dest: "{{ test_basedir }}/{{ item }}" 13 | loop: 14 | - outputs.tf 15 | 16 | - name: Init terraform files 17 | ansible.builtin.shell: | 18 | cd {{ test_basedir }} 19 | terraform init 20 | terraform apply -auto-approve 21 | terraform apply -auto-approve -state mycustomstate.tfstate 22 | 23 | - name: Terraform checkout - default state file + implicit json 24 | cloud.terraform.terraform_output: 25 | project_path: "{{ test_basedir }}" 26 | register: terraform_output 27 | - assert: &json_assert 28 | that: 29 | - terraform_output is not changed 30 | - terraform_output.outputs.my_output.sensitive == false 31 | - terraform_output.outputs.my_output.type == "string" 32 | - terraform_output.outputs.my_output.value == "file generated" 33 | - terraform_output.value is not defined 34 | 35 | - name: Terraform checkout - default state file + explicit json 36 | cloud.terraform.terraform_output: 37 | project_path: "{{ test_basedir }}" 38 | format: json 39 | register: terraform_output 40 | - assert: *json_assert 41 | 42 | - name: Terraform checkout - list only selected output in json format 43 | cloud.terraform.terraform_output: 44 | project_path: "{{ test_basedir }}" 45 | name: my_output 46 | register: terraform_output 47 | - assert: 48 | that: 49 | - terraform_output is not changed 50 | - terraform_output.outputs is not defined 51 | - terraform_output.value == "file generated" 52 | 53 | - name: Terraform checkout - list only selected output in raw format 54 | cloud.terraform.terraform_output: 55 | project_path: "{{ test_basedir }}" 56 | format: raw 57 | name: my_output 58 | register: terraform_output 59 | - assert: 60 | that: 61 | - terraform_output is not changed 62 | - terraform_output.outputs is not defined 63 | - terraform_output.value == "file generated" 64 | 65 | - name: Terraform checkout - specified state file 66 | cloud.terraform.terraform_output: 67 | state_file: "{{ test_basedir }}/mycustomstate.tfstate" 68 | register: terraform_output 69 | - assert: 70 | that: 71 | - terraform_output is not changed 72 | - terraform_output.outputs.my_output.sensitive == false 73 | - terraform_output.outputs.my_output.type == "string" 74 | - terraform_output.outputs.my_output.value == "file generated" 75 | - terraform_output.value is not defined 76 | 77 | - name: Terraform checkout - specified state file and specified output 78 | cloud.terraform.terraform_output: 79 | state_file: "{{ test_basedir }}/mycustomstate.tfstate" 80 | name: my_output 81 | register: terraform_output 82 | - assert: 83 | that: 84 | - terraform_output is not changed 85 | - terraform_output.outputs is not defined 86 | - terraform_output.value == "file generated" 87 | 88 | # terraform lookup using workspace 89 | - name: Test cloud.terraform.terraform_output using workspace 90 | block: 91 | - name: Create temporary directory to work in 92 | ansible.builtin.tempfile: 93 | state: directory 94 | suffix: .tf 95 | register: tf_dir 96 | 97 | - name: Copy terraform files 98 | ansible.builtin.copy: 99 | src: outputs_workspace.tf 100 | dest: "{{ tf_dir.path }}" 101 | 102 | - name: Apply terraform projects 103 | cloud.terraform.terraform: 104 | force_init: true 105 | workspace: '{{ item }}' 106 | project_path: '{{ tf_dir.path }}' 107 | variables: 108 | workspace: "{{ item }}" 109 | with_items: "{{ test_workspaces }}" 110 | 111 | - name: Terraform checkout - specified workspace and project_path 112 | cloud.terraform.terraform_output: 113 | project_path: '{{ tf_dir.path }}' 114 | workspace: '{{ item }}' 115 | register: terraform_output 116 | with_items: "{{ test_workspaces }}" 117 | 118 | - name: Ensure module returned values as expected 119 | ansible.builtin.assert: 120 | that: 121 | - terraform_output.results | map(attribute='outputs.my_workspace.value') | list == test_workspaces 122 | 123 | always: 124 | - name: Delete temporary directory 125 | ansible.builtin.file: 126 | state: absent 127 | path: "{{ tf_dir.path }}" -------------------------------------------------------------------------------- /tests/integration/targets/terraform_plan_file/aliases: -------------------------------------------------------------------------------- 1 | terraform -------------------------------------------------------------------------------- /tests/integration/targets/terraform_plan_file/files/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | ansible = { 4 | source = "ansible/ansible" 5 | } 6 | } 7 | } 8 | 9 | variable "new_group" { 10 | type = string 11 | description = "additional host group" 12 | } 13 | 14 | resource "ansible_host" "my_host" { 15 | name = "localhost" 16 | groups = ["ansible", var.new_group] 17 | variables = { 18 | ansible_user = "ansible" 19 | ansible_host = "127.0.0.1" 20 | } 21 | } 22 | 23 | output "host_groups" { 24 | value = ansible_host.my_host.groups 25 | } -------------------------------------------------------------------------------- /tests/integration/targets/terraform_plan_file/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Testing Terraform plan file custom location 2 | block: 3 | - name: Create temporary directory for terraform project 4 | ansible.builtin.tempfile: 5 | state: directory 6 | suffix: ".terraform_plan_file" 7 | register: tmpdir 8 | 9 | - name: Copy terraform configuration into project path 10 | ansible.builtin.copy: 11 | src: "main.tf" 12 | dest: "{{ tmpdir.path }}/main.tf" 13 | 14 | - name: Create plan file using check_mode=true 15 | cloud.terraform.terraform: 16 | force_init: true 17 | project_path: "{{ tmpdir.path }}" 18 | plan_file: "{{ tmpdir.path }}/test.tfplan" 19 | variables: 20 | new_group: terraform 21 | check_mode: true 22 | 23 | - name: Ensure no resources were created 24 | cloud.terraform.terraform_output: 25 | project_path: "{{ tmpdir.path }}" 26 | register: output 27 | failed_when: output.outputs != {} 28 | 29 | - name: Ensure plan file has been created 30 | ansible.builtin.stat: 31 | path: "{{ tmpdir.path }}/test.tfplan" 32 | register: tfplan 33 | failed_when: not tfplan.stat.exists 34 | 35 | - name: Ensure state file has not been created 36 | ansible.builtin.stat: 37 | path: "{{ tmpdir.path }}/terraform.tfstate" 38 | register: tfstate 39 | failed_when: tfstate.stat.exists 40 | 41 | - name: Apply Terraform generated plan file 42 | cloud.terraform.terraform: 43 | force_init: true 44 | project_path: "{{ tmpdir.path }}" 45 | plan_file: "{{ tmpdir.path }}/test.tfplan" 46 | 47 | - name: Ensure plan file has been applied 48 | cloud.terraform.terraform_output: 49 | project_path: "{{ tmpdir.path }}" 50 | register: output 51 | failed_when: output.outputs.host_groups.value != ["ansible", "terraform"] 52 | 53 | always: 54 | - name: Delete temporary directory 55 | ansible.builtin.file: 56 | state: absent 57 | path: "{{ tmpdir.path }}" -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/ansible_provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | ansible = { 4 | version = ">=1.0" 5 | source = "ansible/ansible" 6 | } 7 | } 8 | } 9 | 10 | resource "ansible_host" "host" { 11 | name = "somehost" 12 | groups = ["somegroup", "anothergroup"] 13 | variables = { 14 | host_hello = "from host!" 15 | host_variable = 7 16 | } 17 | } 18 | 19 | resource "ansible_host" "anotherhost" { 20 | name = "anotherhost" 21 | groups = ["somechild"] 22 | variables = { 23 | host_hello = "from anotherhost!" 24 | host_variable = 5 25 | } 26 | } 27 | 28 | resource "ansible_host" "ungrupedhost" { 29 | name = "ungrupedhost" 30 | } 31 | 32 | resource "ansible_group" "group" { 33 | name = "somegroup" 34 | children = ["somechild", "anotherchild"] 35 | variables = { 36 | group_hello = "from group!", 37 | group_variable = 11 38 | } 39 | } 40 | 41 | resource "ansible_group" "childlessgroup" { 42 | name = "childlessgroup" 43 | } 44 | 45 | module "example" { 46 | source = "./modules/example" 47 | 48 | name = "childhost" 49 | } 50 | 51 | module "nested_module" { 52 | source = "./modules/nested_module" 53 | 54 | name = "nested_module" 55 | } 56 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/inventory1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_provider 3 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/inventory2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_provider 3 | 4 | project_path: subdir2/ 5 | state_file: mycustomstate.tfstate 6 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/inventory3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_provider 3 | 4 | project_path: subdir3/ 5 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/inventory4.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: cloud.terraform.terraform_provider 3 | 4 | state_file: subdir4/mystate.tfstate 5 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/modules/example/main.tf: -------------------------------------------------------------------------------- 1 | resource "ansible_host" "childhost" { 2 | name = var.name 3 | 4 | } 5 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/modules/example/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | ansible = { 4 | version = ">=1.0" 5 | source = "ansible/ansible" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/modules/example/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | } -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/modules/nested_module/main.tf: -------------------------------------------------------------------------------- 1 | module "example" { 2 | source = "../example" 3 | 4 | name = "nested_childhost" 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/modules/nested_module/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | ansible = { 4 | version = ">=1.0" 5 | source = "ansible/ansible" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/modules/nested_module/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | } -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/runme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | ( 6 | mkdir -p subdir2/ 7 | cp test.yml subdir2/ 8 | cp ansible_provider.tf subdir2/ 9 | cp -r modules subdir2 10 | cd subdir2/ 11 | terraform init 12 | terraform apply -auto-approve -state mycustomstate.tfstate 13 | ) 14 | ansible-playbook -i localhost, -i inventory2.yml test.yml 15 | 16 | ( 17 | mkdir -p subdir3/ 18 | cp test.yml subdir3/ 19 | cp ansible_provider.tf subdir3/ 20 | cp -r modules subdir3 21 | cd subdir3/ 22 | terraform init 23 | terraform apply -auto-approve 24 | ) 25 | ansible-playbook -i localhost, -i inventory3.yml test.yml 26 | 27 | ( 28 | mkdir -p subdir4/ 29 | cp test.yml subdir4/ 30 | cp ansible_provider.tf subdir4/ 31 | cp -r modules subdir4 32 | cd subdir4/ 33 | terraform init 34 | terraform apply -auto-approve -state mystate.tfstate 35 | cd .. 36 | terraform init 37 | ) 38 | ansible-playbook -i localhost, -i inventory4.yml test.yml 39 | 40 | ( 41 | terraform init 42 | terraform apply -auto-approve 43 | ) 44 | ansible-playbook -i localhost, -i inventory1.yml test.yml 45 | -------------------------------------------------------------------------------- /tests/integration/targets/terraform_provider/test.yml: -------------------------------------------------------------------------------- 1 | - name: Test create groups 2 | hosts: localhost 3 | gather_facts: false 4 | tasks: 5 | - ansible.builtin.assert: 6 | that: 7 | - groups | length == 7 8 | - "'all' in groups" 9 | - "'ungrouped' in groups" 10 | - "'childlessgroup' in groups" 11 | - "'somegroup' in groups" 12 | - "'anothergroup' in groups" 13 | - "'somechild' in groups" 14 | - "'anotherchild' in groups" 15 | 16 | - name: Test create hosts 17 | hosts: localhost 18 | gather_facts: false 19 | tasks: 20 | - ansible.builtin.assert: 21 | that: 22 | - "'somehost' in hostvars" 23 | - "'anotherhost' in hostvars" 24 | - "'childhost' in hostvars" 25 | - "'nested_childhost' in hostvars" 26 | 27 | - name: Test host and group variables 28 | hosts: localhost 29 | gather_facts: false 30 | tasks: 31 | - ansible.builtin.assert: 32 | that: 33 | - hostvars["somehost"].host_hello == "from host!" 34 | - hostvars["somehost"].host_variable == "7" 35 | - hostvars["somehost"].group_hello == "from group!" 36 | - hostvars["somehost"].group_variable == "11" 37 | - hostvars["anotherhost"].host_hello == "from anotherhost!" 38 | - hostvars["anotherhost"].host_variable == "5" 39 | 40 | - name: Test asigning hosts to groups 41 | hosts: localhost 42 | gather_facts: false 43 | tasks: 44 | - ansible.builtin.assert: 45 | that: 46 | - "'somegroup' in hostvars['somehost']['groups']" 47 | - "'anothergroup' in hostvars['somehost']['groups']" 48 | - "'somechild' in hostvars['anotherhost']['groups']" 49 | - "'ungrouped' in hostvars['ungrupedhost']['groups']" 50 | -------------------------------------------------------------------------------- /tests/integration/targets/test_git_plan/files/write_file.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "2.2.3" 6 | } 7 | } 8 | } 9 | 10 | provider "local" { 11 | # Configuration options (I have none :_) 12 | } 13 | 14 | resource "local_file" "foo" { 15 | content = "This file was written by terraform!" 16 | filename = "${path.module}/terraform_test.txt" 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tests/integration/targets/test_git_plan/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | test_basedir: "{{ test_basedir | default(output_dir) }}" 3 | 4 | - name: Create terraform project directories 5 | ansible.builtin.file: 6 | path: "{{ test_basedir }}/{{ item }}/" 7 | state: directory 8 | mode: 0755 9 | loop: 10 | - origin 11 | - repo 12 | 13 | - name: Copy terraform files to work space 14 | ansible.builtin.copy: 15 | src: "{{ item }}" 16 | dest: "{{ test_basedir }}/origin/" 17 | loop: 18 | - write_file.tf 19 | 20 | - name: Make a repository 21 | ansible.builtin.shell: | 22 | set -eu 23 | cd {{ test_basedir }}/origin/ 24 | terraform init 25 | terraform plan -out myplan.tfplan 26 | git init 27 | echo ".terraform*" > .gitignore 28 | git add . 29 | git commit -m "Initial commit." 30 | git branch targetbranch 31 | 32 | - &stat 33 | name: Run stat on the result file 34 | stat: 35 | path: "{{ test_basedir }}/repo/terraform_test.txt" 36 | register: stat 37 | - name: The test file must not exist 38 | assert: 39 | that: not stat.stat.exists 40 | 41 | - name: Call the role 42 | ansible.builtin.include_role: 43 | name: cloud.terraform.git_plan 44 | vars: 45 | repo_url: "file://{{ test_basedir }}/origin/" 46 | repo_dir: "{{ test_basedir }}/repo/" 47 | version: targetbranch 48 | plan_file: myplan.tfplan 49 | terraform_options: 50 | force_init: true 51 | 52 | - *stat 53 | - name: The test file must exist 54 | assert: 55 | that: stat.stat.exists 56 | -------------------------------------------------------------------------------- /tests/integration/targets/test_inventory_from_outputs/files/create_inventory.tf: -------------------------------------------------------------------------------- 1 | output "myvar_hostlist" { 2 | value = [ 3 | { "myvar_ip" : "my_ip1", "myvar_group" : "my_group1", "myvar_name" : "my_name1", "myvar_user" : "my_user" }, 4 | { "myvar_ip" : "my_ip2", "myvar_group" : "my_group2", "myvar_name" : "my_name2", "myvar_user" : "my_user" }, 5 | { "myvar_ip" : "my_ip3", "myvar_group" : "my_group1", "myvar_name" : "my_name3", "myvar_user" : "my_user" }, 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/targets/test_inventory_from_outputs/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - set_fact: 2 | test_basedir: "{{ test_basedir | default(output_dir) }}" 3 | 4 | - name: Copy terraform files to work space 5 | ansible.builtin.copy: 6 | src: "{{ item }}" 7 | dest: "{{ test_basedir }}/{{ item }}" 8 | loop: 9 | - create_inventory.tf 10 | 11 | - name: Init terraform files 12 | ansible.builtin.shell: | 13 | cd {{ test_basedir }} 14 | terraform init 15 | terraform apply -auto-approve 16 | 17 | - name: Create the inventory 18 | ansible.builtin.include_role: 19 | name: cloud.terraform.inventory_from_outputs 20 | vars: 21 | project_path: "{{ test_basedir }}" 22 | state_file: "{{ test_basedir }}/terraform.tfstate" 23 | mapping_variables: 24 | host_list: myvar_hostlist 25 | name: myvar_name 26 | ip: myvar_ip 27 | user: myvar_user 28 | group: myvar_group 29 | 30 | - name: Group assertions 31 | assert: 32 | that: 33 | - groups.my_group1 is defined 34 | - (groups.my_group1 | length) == 2 35 | - "'my_name1' in groups.my_group1" 36 | - "'my_name3' in groups.my_group1" 37 | - (groups.ungrouped | length) == 0 38 | 39 | - name: Host assertions 40 | assert: 41 | that: 42 | - "'my_name2' in hostvars" 43 | - hostvars.my_name2.ansible_host == "my_ip2" 44 | - hostvars.my_name2.ansible_user == "my_user" 45 | -------------------------------------------------------------------------------- /tests/unit/plugins/lookup/test_tf_output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | from subprocess import CompletedProcess 8 | 9 | from ansible_collections.cloud.terraform.plugins.lookup import tf_output 10 | 11 | 12 | class TestModuleRunCommand: 13 | def test_module_run_command(self, mocker): 14 | mocker.patch( 15 | "ansible_collections.cloud.terraform.plugins.lookup.tf_output.subprocess.run" 16 | ).return_value = CompletedProcess( 17 | args="args", 18 | returncode="returncode", 19 | stdout="stdout".encode("utf-8"), 20 | stderr="stderr".encode("utf-8"), 21 | ) 22 | result = tf_output.module_run_command(["commands"], "cwd") 23 | 24 | assert result == ("returncode", "stdout", "stderr") 25 | 26 | 27 | class TestLookupModuleRun: 28 | def test_run_with_terms(self, mocker): 29 | mocker.patch("ansible_collections.cloud.terraform.plugins.lookup.tf_output.LookupModule.set_options") 30 | mocker.patch( 31 | "ansible_collections.cloud.terraform.plugins.lookup.tf_output.LookupModule.get_option" 32 | ).side_effect = ["project_path", "state_file", "bin_path", "workspace"] 33 | mocker.patch("ansible_collections.cloud.terraform.plugins.lookup.tf_output.process.get_bin_path") 34 | mocker.patch("ansible_collections.cloud.terraform.plugins.lookup.tf_output.get_outputs").side_effect = [ 35 | "my_output_value1", 36 | "my_output_value2", 37 | ] 38 | 39 | my_module = tf_output.LookupModule() 40 | output = my_module.run(terms=["my_output1", "my_output2"]) 41 | 42 | assert output == ["my_output_value1", "my_output_value2"] 43 | 44 | def test_run_without_terms(self, mocker): 45 | mocker.patch("ansible_collections.cloud.terraform.plugins.lookup.tf_output.LookupModule.set_options") 46 | mocker.patch( 47 | "ansible_collections.cloud.terraform.plugins.lookup.tf_output.LookupModule.get_option" 48 | ).side_effect = ["project_path", "state_file", "bin_path", "workspace"] 49 | mocker.patch("ansible_collections.cloud.terraform.plugins.lookup.tf_output.process.get_bin_path") 50 | mocker.patch("ansible_collections.cloud.terraform.plugins.lookup.tf_output.get_outputs").return_value = { 51 | "my_output1": {"sensitive": False, "type": "string", "value": "value1"}, 52 | "my_output2": {"sensitive": False, "type": "string", "value": "value2"}, 53 | } 54 | 55 | my_module = tf_output.LookupModule() 56 | output = my_module.run(terms=None) 57 | 58 | assert output == [ 59 | { 60 | "my_output1": {"sensitive": False, "type": "string", "value": "value1"}, 61 | "my_output2": {"sensitive": False, "type": "string", "value": "value2"}, 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_models.py: -------------------------------------------------------------------------------- 1 | from ansible_collections.cloud.terraform.plugins.module_utils.models import TerraformModuleResource 2 | 3 | 4 | class TestTerraformModuleResource: 5 | def test_from_json(self): 6 | json_data = { 7 | "address": "aws_iam_role.this", 8 | "mode": "managed", 9 | "type": "aws_iam_role", 10 | "name": "this", 11 | "provider_name": "registry.terraform.io/hashicorp/aws", 12 | "schema_version": 0, 13 | "values": { 14 | "assume_role_policy": ( 15 | '{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow",' 16 | '"Principal":{"Service":"lambda.amazonaws.com"},"Sid":""}],"Version":"2012-10-17"}' 17 | ), 18 | "description": None, 19 | "force_detach_policies": False, 20 | "max_session_duration": 3600, 21 | "name": "ansible-test-59219564-jcpc-sqs-role", 22 | "path": "/", 23 | "permissions_boundary": None, 24 | "tags": { 25 | "Name": "ansible-test-59219564-jcpc-sqs", 26 | "cloud_terraform_integration": "true", 27 | }, 28 | "tags_all": { 29 | "Name": "ansible-test-59219564-jcpc-sqs", 30 | "cloud_terraform_integration": "true", 31 | }, 32 | }, 33 | "sensitive_values": { 34 | "inline_policy": [], 35 | "managed_policy_arns": [], 36 | "tags": {}, 37 | "tags_all": {}, 38 | }, 39 | } 40 | 41 | tfm = TerraformModuleResource.from_json(json_data) 42 | 43 | assert "aws_iam_role.this" == tfm.address 44 | assert "managed" == tfm.mode 45 | assert "aws_iam_role" == tfm.type 46 | assert "this" == tfm.name 47 | assert "registry.terraform.io/hashicorp/aws" == tfm.provider_name 48 | assert 0 == tfm.schema_version 49 | # potentially undefined 50 | assert json_data["values"] == tfm.values 51 | assert json_data["sensitive_values"] == tfm.sensitive_values 52 | assert [] == tfm.depends_on 53 | 54 | def test_from_json__missing_values(self): 55 | json_data = { 56 | "address": "aws_sqs_queue_policy.this", 57 | "mode": "managed", 58 | "type": "aws_sqs_queue_policy", 59 | "name": "this", 60 | "provider_name": "registry.terraform.io/hashicorp/aws", 61 | "schema_version": 1, 62 | "sensitive_values": {}, 63 | } 64 | 65 | tfm = TerraformModuleResource.from_json(json_data) 66 | 67 | assert "aws_sqs_queue_policy.this" == tfm.address 68 | assert "managed" == tfm.mode 69 | assert "aws_sqs_queue_policy" == tfm.type 70 | assert "this" == tfm.name 71 | assert "registry.terraform.io/hashicorp/aws" == tfm.provider_name 72 | assert 1 == tfm.schema_version 73 | # potentially undefined 74 | assert {} == tfm.values 75 | assert {} == tfm.sensitive_values 76 | assert [] == tfm.depends_on 77 | -------------------------------------------------------------------------------- /tests/unit/plugins/module_utils/test_terraform_commands.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | from ansible.module_utils.compat.version import LooseVersion 5 | from ansible_collections.cloud.terraform.plugins.module_utils.terraform_commands import ( 6 | TerraformCommands, 7 | WorkspaceCommand, 8 | ) 9 | 10 | 11 | class TestTerraformCommands: 12 | def setup_method(self): 13 | self.mock = MagicMock() 14 | self.tf = TerraformCommands(self.mock, "/project/path", "/binary/path", False) 15 | self.rc, self.stdout, self.stderr = 0, "", "" 16 | 17 | def test_run(self): 18 | args = ["apply", "-no-color", "-input=false"] 19 | self.tf._run(*args, check_rc=False) 20 | 21 | # Testing if self.run_command_fp(...) was called. 22 | # self.run_command_fp(...) should be called in this method, 23 | # therefore the test will pass if self.run_command_fp(...) was called 24 | self.mock.assert_called_with(["/binary/path"] + args, cwd="/project/path", check_rc=False) 25 | 26 | def test_apply_plan(self): 27 | self.mock.return_value = (self.rc, self.stdout, self.stderr) 28 | self.tf._run = self.mock 29 | self.tf.apply_plan( 30 | plan_file_path="/plan/path", 31 | version=LooseVersion("0.15.0"), 32 | parallelism=0, 33 | lock=False, 34 | lock_timeout=0, 35 | targets=["target_file"], 36 | needs_application=True, 37 | ) 38 | 39 | # Expected command to be called with self._run(...) 40 | expected_cmd = [ 41 | "apply", 42 | "-no-color", 43 | "-input=false", 44 | "-auto-approve", 45 | "-parallelism=0", 46 | "-lock=false", 47 | "-lock-timeout=0s", 48 | "-target", 49 | "target_file", 50 | "/plan/path", 51 | ] 52 | 53 | self.mock.assert_called_with(*expected_cmd, check_rc=False) 54 | 55 | # Test init method; NOT __init__ 56 | def test_init(self): 57 | self.tf._run = self.mock 58 | self.tf.init( 59 | backend_config={"test_val": "test"}, 60 | backend_config_files=["config_file"], 61 | reconfigure=True, 62 | upgrade=True, 63 | plugin_paths=["/plugin/path"], 64 | ) 65 | 66 | expected_cmd = [ 67 | "init", 68 | "-input=false", 69 | "-no-color", 70 | "-backend-config", 71 | "test_val=test", 72 | "-backend-config", 73 | "config_file", 74 | "-reconfigure", 75 | "-upgrade", 76 | "-plugin-dir", 77 | "/plugin/path", 78 | ] 79 | 80 | self.mock.assert_called_with(*expected_cmd, check_rc=True) 81 | 82 | def test_plan(self): 83 | self.mock.return_value = (self.rc, self.stdout, self.stderr) 84 | self.tf._run = self.mock 85 | self.tf.plan( 86 | target_plan_file_path="/target/plan/file", 87 | targets=["target_file"], 88 | destroy=True, 89 | state_args=["state_arg"], 90 | variables_args=["var_arg"], 91 | ) 92 | 93 | expected_cmd = [ 94 | "plan", 95 | "-lock=true", 96 | "-input=false", 97 | "-no-color", 98 | "-detailed-exitcode", 99 | "-out", 100 | "/target/plan/file", 101 | "-target", 102 | "target_file", 103 | "-destroy", 104 | "state_arg", 105 | "var_arg", 106 | ] 107 | 108 | self.mock.assert_called_with(*expected_cmd, check_rc=False) 109 | 110 | def test_providers_schema(self): 111 | self.stdout = '{"format_version":"1.0"}' 112 | self.mock.return_value = (self.rc, self.stdout, self.stderr) 113 | self.tf._run = self.mock 114 | self.tf.providers_schema() 115 | 116 | expected_cmd = ["providers", "schema", "-json"] 117 | 118 | self.mock.assert_called_with(*expected_cmd, check_rc=False) 119 | 120 | def test_show(self): 121 | self.stdout = '{"format_version":"1.0"}' 122 | self.mock.return_value = (self.rc, self.stdout, self.stderr) 123 | self.tf._run = self.mock 124 | self.tf.show("/state/or/plan/file") 125 | 126 | expected_cmd = ["show", "-json", "/state/or/plan/file"] 127 | 128 | self.mock.assert_called_with(*expected_cmd, check_rc=False) 129 | 130 | def test_validate(self): 131 | self.tf._run = self.mock 132 | self.tf.validate(LooseVersion("0.15.0"), ["var_arg"]) 133 | 134 | expected_cmd = ["validate"] 135 | 136 | self.mock.assert_called_with(*expected_cmd, check_rc=True) 137 | 138 | def test_version(self): 139 | self.stdout = '{ \ 140 | "terraform_version": "1.3.6", \ 141 | "platform": "linux_amd64", \ 142 | "provider_selections": {}, \ 143 | "terraform_outdated": true \ 144 | }' 145 | self.mock.return_value = (self.rc, self.stdout, self.stderr) 146 | self.tf._run = self.mock 147 | 148 | self.tf.version() 149 | 150 | expected_cmd = ["version", "-json"] 151 | self.mock.assert_called_with(*expected_cmd, check_rc=True) 152 | 153 | @pytest.mark.parametrize( 154 | "test_workspace_cmd, expected_workspace_cmd_value", 155 | [(WorkspaceCommand.NEW, "new"), (WorkspaceCommand.SELECT, "select"), (WorkspaceCommand.DELETE, "delete")], 156 | ) 157 | def test_workspace(self, test_workspace_cmd, expected_workspace_cmd_value): 158 | self.tf._run = self.mock 159 | self.tf.workspace(test_workspace_cmd, "/workspace/path") 160 | expected_cmd = ["workspace", expected_workspace_cmd_value, "/workspace/path"] 161 | self.mock.assert_called_with(*expected_cmd, check_rc=True) 162 | 163 | def test_workspace_list(self): 164 | self.mock.return_value = (self.rc, self.stdout, self.stderr) 165 | self.tf._run = self.mock 166 | self.tf.workspace_list() 167 | expected_cmd = ["workspace", "list", "-no-color"] 168 | self.mock.assert_called_with(*expected_cmd, check_rc=False) 169 | -------------------------------------------------------------------------------- /tests/unit/plugins/modules/cloud/misc/test_terraform.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, Ansible Project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | import json 5 | 6 | import pytest 7 | from ansible_collections.cloud.terraform.plugins.modules import terraform 8 | from ansible_collections.cloud.terraform.tests.unit.plugins.modules.utils import set_module_args 9 | 10 | 11 | def test_terraform_without_argument(capfd): 12 | with set_module_args({}): 13 | with pytest.raises(SystemExit): 14 | terraform.main() 15 | 16 | out, err = capfd.readouterr() 17 | assert not err 18 | assert json.loads(out)["failed"] 19 | assert "project_path" in json.loads(out)["msg"] 20 | -------------------------------------------------------------------------------- /tests/unit/plugins/modules/test_terraform_output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | import pytest 8 | from ansible.module_utils import basic 9 | from ansible_collections.cloud.terraform.plugins.module_utils.errors import TerraformError, TerraformWarning 10 | from ansible_collections.cloud.terraform.plugins.modules import terraform_output 11 | from ansible_collections.cloud.terraform.tests.unit.plugins.modules.utils import ( 12 | AnsibleExitJson, 13 | AnsibleFailJson, 14 | exit_json, 15 | fail_json, 16 | set_module_args, 17 | ) 18 | 19 | 20 | class TestTerraformOutputMain: 21 | def test_return_selected_output(self, mocker): 22 | mocker.patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) 23 | mocker.patch("ansible_collections.cloud.terraform.plugins.modules.terraform_output.validate_bin_path") 24 | mocker.patch( 25 | "ansible_collections.cloud.terraform.plugins.modules.terraform_output.get_outputs" 26 | ).return_value = "my_output_value" 27 | 28 | with set_module_args( 29 | { 30 | "project_path": "path/to/project", 31 | "name": "my_output", 32 | "format": "json", 33 | "binary_path": "terraform/binary/path", 34 | "state_file": "mystate.tfstate", 35 | } 36 | ): 37 | with pytest.raises(AnsibleExitJson) as exc: 38 | terraform_output.main() 39 | 40 | assert exc.value.args[0]["value"] == "my_output_value" 41 | 42 | def test_return_all_outputs(self, mocker): 43 | mocker.patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) 44 | mocker.patch("ansible_collections.cloud.terraform.plugins.modules.terraform_output.validate_bin_path") 45 | mocker.patch( 46 | "ansible_collections.cloud.terraform.plugins.modules.terraform_output.get_outputs" 47 | ).return_value = {"my_output": {"sensitive": False, "type": "string", "value": "file generated"}} 48 | 49 | with set_module_args( 50 | { 51 | "project_path": "path/to/project", 52 | "format": "json", 53 | "binary_path": "terraform/binary/path", 54 | "state_file": "mystate.tfstate", 55 | } 56 | ): 57 | with pytest.raises(AnsibleExitJson) as exc: 58 | terraform_output.main() 59 | 60 | assert exc.value.args[0]["outputs"] == { 61 | "my_output": {"sensitive": False, "type": "string", "value": "file generated"} 62 | } 63 | 64 | def test_required_if(self, mocker): 65 | mocker.patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) 66 | mocker.patch("ansible_collections.cloud.terraform.plugins.modules.terraform_output.validate_bin_path") 67 | mocker.patch("ansible_collections.cloud.terraform.plugins.modules.terraform_output.get_outputs") 68 | 69 | with set_module_args( 70 | { 71 | "project_path": "path/to/project", 72 | "format": "raw", 73 | "binary_path": "terraform/binary/path", 74 | "state_file": "mystate.tfstate", 75 | } 76 | ): 77 | with pytest.raises(AnsibleFailJson) as exc: 78 | terraform_output.main() 79 | 80 | assert exc.value.args[0]["msg"] == "format is raw but all of the following are missing: name" 81 | 82 | def test_except_terraform_warning(self, mocker): 83 | mocker.patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) 84 | mocker.patch("ansible_collections.cloud.terraform.plugins.modules.terraform_output.validate_bin_path") 85 | mocker.patch( 86 | "ansible_collections.cloud.terraform.plugins.modules.terraform_output.get_outputs" 87 | ).side_effect = TerraformWarning("Could not get Terraform outputs.") 88 | 89 | with set_module_args( 90 | { 91 | "project_path": "path/to/project", 92 | "name": "my_output", 93 | "format": "json", 94 | "binary_path": "terraform/binary/path", 95 | "state_file": "mystate.tfstate", 96 | } 97 | ): 98 | with pytest.raises(AnsibleExitJson) as exc: 99 | terraform_output.main() 100 | 101 | assert exc.value.args[0]["value"] is None 102 | 103 | def test_except_terraform_error(self, mocker): 104 | mocker.patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) 105 | mocker.patch("ansible_collections.cloud.terraform.plugins.modules.terraform_output.validate_bin_path") 106 | mocker.patch( 107 | "ansible_collections.cloud.terraform.plugins.modules.terraform_output.get_outputs" 108 | ).side_effect = TerraformError("Failure when getting Terraform outputs.") 109 | with set_module_args( 110 | { 111 | "project_path": "path/to/project", 112 | "name": "my_output", 113 | "format": "json", 114 | "binary_path": "terraform/binary/path", 115 | "state_file": "mystate.tfstate", 116 | } 117 | ): 118 | with pytest.raises(AnsibleFailJson) as exc: 119 | terraform_output.main() 120 | 121 | assert exc.value.args[0]["msg"] == "Failure when getting Terraform outputs." 122 | -------------------------------------------------------------------------------- /tests/unit/plugins/modules/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ansible project 2 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import contextlib 6 | import json 7 | import unittest 8 | from unittest.mock import patch 9 | 10 | from ansible.module_utils import basic 11 | from ansible.module_utils.common.text.converters import to_bytes 12 | 13 | 14 | @contextlib.contextmanager 15 | def set_module_args(args): 16 | """ 17 | Context manager that sets module arguments for AnsibleModule 18 | """ 19 | if "_ansible_remote_tmp" not in args: 20 | args["_ansible_remote_tmp"] = "/tmp" 21 | if "_ansible_keep_remote_files" not in args: 22 | args["_ansible_keep_remote_files"] = False 23 | 24 | try: 25 | from ansible.module_utils.testing import patch_module_args 26 | except ImportError: 27 | # Before data tagging support was merged, this was the way to go: 28 | from ansible.module_utils import basic 29 | 30 | serialized_args = to_bytes(json.dumps({"ANSIBLE_MODULE_ARGS": args})) 31 | with patch.object(basic, "_ANSIBLE_ARGS", serialized_args): 32 | yield 33 | else: 34 | # With data tagging support, we have a new helper for this: 35 | with patch_module_args(args): 36 | yield 37 | 38 | 39 | class AnsibleExitJson(Exception): 40 | pass 41 | 42 | 43 | class AnsibleFailJson(Exception): 44 | pass 45 | 46 | 47 | def exit_json(*args, **kwargs): 48 | if "changed" not in kwargs: 49 | kwargs["changed"] = False 50 | raise AnsibleExitJson(kwargs) 51 | 52 | 53 | def fail_json(*args, **kwargs): 54 | kwargs["failed"] = True 55 | raise AnsibleFailJson(kwargs) 56 | 57 | 58 | class ModuleTestCase(unittest.TestCase): 59 | def setUp(self): 60 | self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) 61 | self.mock_module.start() 62 | self.mock_sleep = patch("time.sleep") 63 | self.mock_sleep.start() 64 | set_module_args({}) 65 | self.addCleanup(self.mock_module.stop) 66 | self.addCleanup(self.mock_sleep.stop) 67 | -------------------------------------------------------------------------------- /tests/unit/plugins/plugin_utils/test_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (c) 2022, XLAB Steampunk 3 | # 4 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | from subprocess import CompletedProcess 7 | 8 | from ansible_collections.cloud.terraform.plugins.plugin_utils.common import module_run_command 9 | 10 | 11 | class TestModuleRunCommand: 12 | def test_module_run_command(self, mocker): 13 | cmd = ["test"] 14 | cwd = "test/directory" 15 | mocker.patch( 16 | "ansible_collections.cloud.terraform.plugins.plugin_utils.common.subprocess.run" 17 | ).return_value = CompletedProcess( 18 | args=cmd, 19 | returncode=0, 20 | stdout="stdout".encode("utf-8"), 21 | stderr="stderr".encode("utf-8"), 22 | ) 23 | 24 | completed_process = module_run_command(cmd=cmd, cwd=cwd, check_rc=False) 25 | 26 | assert completed_process == (0, "stdout", "stderr") 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.0 3 | skipsdist = True 4 | 5 | [common] 6 | format_dirs = {toxinidir}/plugins {toxinidir}/tests 7 | 8 | [testenv] 9 | usedevelop = True 10 | setenv = VIRTUAL_ENV={envdir} 11 | PYTHONHASHSEED=0 12 | PYTHONWARNINGS=default::DeprecationWarning 13 | passenv = PYTHONPATH 14 | install_command = pip install {opts} {packages} 15 | deps = -r {toxinidir}/test-requirements.txt 16 | 17 | [testenv:add_docs] 18 | deps = 19 | git+https://github.com/ansible-network/collection_prep 20 | commands = 21 | collection_prep_add_docs -p . 22 | 23 | [testenv:antsibull-docs] 24 | deps = 25 | aiohttp>=3.9.0b1 # required for python 3.12 26 | antsibull-docs 27 | commands = 28 | antsibull-docs lint-collection-docs . 29 | 30 | [testenv:black] 31 | deps = 32 | black==23.12.1 33 | commands = 34 | black --check --diff {[common]format_dirs} 35 | 36 | [testenv:black_format] 37 | deps = 38 | {[testenv:black]deps} 39 | commands = 40 | black -v {[common]format_dirs} 41 | 42 | [testenv:flake8] 43 | deps = 44 | flake8 45 | commands = 46 | flake8 {[common]format_dirs} 47 | 48 | [testenv:isort] 49 | deps = 50 | isort 51 | commands = 52 | isort --check-only --diff {[common]format_dirs} 53 | 54 | [testenv:isort_format] 55 | deps = 56 | {[testenv:isort]deps} 57 | commands = 58 | isort -v {[common]format_dirs} 59 | 60 | [testenv:mypy] 61 | allowlist_externals = bash 62 | deps = 63 | mypy 64 | pytest 65 | pytest-forked 66 | pytest-mock 67 | pytest-xdist 68 | types-PyYAML 69 | packaging 70 | requests[security] 71 | xmltodict 72 | setenv = 73 | {[testenv]setenv} 74 | MYPYPATH={toxinidir} 75 | commands = bash {toxinidir}/run_mypy.sh 76 | 77 | [testenv:format] 78 | deps = 79 | {[testenv:black]deps} 80 | {[testenv:isort]deps} 81 | commands = 82 | {[testenv:black]commands} 83 | {[testenv:isort]commands} 84 | 85 | [testenv:linters] 86 | allowlist_externals = 87 | {[testenv:mypy]allowlist_externals} 88 | deps = 89 | {[testenv:antsibull-docs]deps} 90 | {[testenv:black]deps} 91 | {[testenv:flake8]deps} 92 | {[testenv:isort]deps} 93 | {[testenv:mypy]deps} 94 | commands = 95 | {[testenv:antsibull-docs]commands} 96 | {[testenv:black]commands} 97 | {[testenv:flake8]commands} 98 | {[testenv:isort]commands} 99 | {[testenv:mypy]commands} 100 | 101 | [flake8] 102 | ignore = E402 103 | max-line-length = 160 104 | show-source = True 105 | --------------------------------------------------------------------------------