├── changelogs
├── fragments
│ └── .keep
├── config.yaml
└── changelog.yaml
├── tests
├── vault-encrypted.yml
├── terraform_tests
│ ├── vault_password
│ ├── vault-decrypted.yml
│ ├── ansible-dev.tfrc
│ ├── playbooks
│ │ ├── play-test-default.yml
│ │ ├── play-test-other.yml
│ │ └── play-vault-test.yml
│ ├── run_tftest.sh
│ └── main.tf
├── integration
│ └── provider_test.go
└── expected_tfstate.json
├── examples
├── ansible_playbook
│ ├── vault-password-file.txt
│ ├── var-file.yml
│ ├── Dockerfile
│ ├── vault-file.yml
│ ├── main.tf
│ ├── simple-playbook.yml
│ ├── end-to-end-playbook.yml
│ ├── end-to-end-expected-output
│ ├── simple.tf
│ ├── README.md
│ └── end-to-end.tf
├── aws
│ ├── inventory.yml
│ ├── runme.sh
│ ├── playbook.yml
│ └── main.tf
├── resources
│ ├── ansible_vault
│ │ └── resource.tf
│ ├── ansible_group
│ │ └── resource.tf
│ ├── ansible_playbook
│ │ └── resource.tf
│ └── ansible_host
│ │ └── resource.tf
├── actions
│ └── ansible_playbook_run
│ │ └── action.tf
├── main.tf
├── provider
│ └── provider.tf
└── data-sources
│ └── ansible_inventory
│ └── data-source.tf
├── Makefile
├── terraform-registry-manifest.json
├── .github
├── patchback.yml
└── workflows
│ ├── changelog.yml
│ ├── integration.yml
│ ├── linters.yml
│ └── release.yml
├── tools
└── tools.go
├── .gitignore
├── templates
├── index.md.tmpl
├── resources
│ ├── vault.md.tmpl
│ ├── host.md.tmpl
│ ├── group.md.tmpl
│ └── playbook.md.tmpl
├── actions
│ └── playbook_run.md.tmpl
└── data-sources
│ └── inventory.md.tmpl
├── provider
├── provider.go
├── resource_host.go
├── resource_group.go
├── resource_vault.go
└── resource_playbook.go
├── .golangci.yml
├── docs
├── resources
│ ├── group.md
│ ├── vault.md
│ ├── host.md
│ └── playbook.md
├── index.md
├── actions
│ └── playbook_run.md
└── data-sources
│ └── inventory.md
├── main.go
├── CHANGELOG.rst
├── .goreleaser.yml
├── framework
├── provider.go
├── data_inventory.go
└── action_playbook_run.go
├── README.md
├── go.mod
├── providerutils
└── utils.go
└── go.sum
/changelogs/fragments/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/vault-encrypted.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/terraform_tests/vault_password:
--------------------------------------------------------------------------------
1 | password
--------------------------------------------------------------------------------
/examples/ansible_playbook/vault-password-file.txt:
--------------------------------------------------------------------------------
1 | password
--------------------------------------------------------------------------------
/examples/aws/inventory.yml:
--------------------------------------------------------------------------------
1 | ---
2 | plugin: cloud.terraform.terraform_provider
3 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/var-file.yml:
--------------------------------------------------------------------------------
1 | content_from_a_var_file: "content from a var file"
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | go build -o terraform-provider-ansible
3 |
4 | test: build
5 | cd tests/terraform_tests && ./run_tftest.sh
6 |
--------------------------------------------------------------------------------
/tests/terraform_tests/vault-decrypted.yml:
--------------------------------------------------------------------------------
1 | hello: from vault!
2 | a_number: 24356
3 | a_list:
4 | - some
5 | - nice
6 | - list
--------------------------------------------------------------------------------
/terraform-registry-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "metadata": {
4 | "protocol_versions": ["5.0"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.github/patchback.yml:
--------------------------------------------------------------------------------
1 | ---
2 | backport_branch_prefix: patchback/backports/
3 | backport_label_prefix: backport-
4 | target_branch_prefix: stable-
5 |
--------------------------------------------------------------------------------
/tests/terraform_tests/ansible-dev.tfrc:
--------------------------------------------------------------------------------
1 | provider_installation {
2 | dev_overrides {
3 | "ansible/ansible" = "../../.."
4 | }
5 |
6 | direct {}
7 | }
8 |
--------------------------------------------------------------------------------
/examples/resources/ansible_vault/resource.tf:
--------------------------------------------------------------------------------
1 | resource "ansible_vault" "secrets" {
2 | vault_file = "vault.yml"
3 | vault_password_file = "/path/to/file"
4 | }
5 |
--------------------------------------------------------------------------------
/tests/terraform_tests/playbooks/play-test-default.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: A test playbook
3 | hosts: localhost
4 |
5 | vars:
6 | docker_name: "{{ docker_container }}"
7 |
8 |
--------------------------------------------------------------------------------
/tests/terraform_tests/playbooks/play-test-other.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: A test playbook
3 | hosts: localhost
4 |
5 | vars:
6 | docker_name: "{{ docker_container }}"
7 |
8 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 |
3 | package tools
4 |
5 | import (
6 | // document generation
7 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs"
8 | )
9 |
10 |
--------------------------------------------------------------------------------
/examples/resources/ansible_group/resource.tf:
--------------------------------------------------------------------------------
1 | resource "ansible_group" "group" {
2 | name = "somegroup"
3 | children = ["somechild"]
4 | variables = {
5 | hello = "from group!"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN \
3 | apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python \
4 | && python3 -m ensurepip \
5 | && pip3 install --no-cache --upgrade pip setuptools
6 |
--------------------------------------------------------------------------------
/examples/aws/runme.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -eux
4 |
5 | terraform init
6 | terraform apply -auto-approve
7 |
8 | ansible-playbook -i inventory.yml playbook.yml
9 |
10 | ip=$(ansible-inventory -i inventory.yml --list | jq -r '.nginx.hosts[0]')
11 | curl "http://${ip}" --fail
12 |
--------------------------------------------------------------------------------
/examples/resources/ansible_playbook/resource.tf:
--------------------------------------------------------------------------------
1 | resource "ansible_playbook" "playbook" {
2 | playbook = "playbook.yml"
3 | name = "host-1.example.com"
4 | replayable = true
5 |
6 | extra_vars = {
7 | var_a = "Some variable"
8 | var_b = "Another variable"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .terraform
2 | .terraform.lock.hcl
3 | actual_tfstate.json
4 | terraform.tfstate
5 | terraform.tfstate.backup
6 |
7 | # binaries
8 | act
9 | golangci-lint
10 | terraform-provider-ansible
11 | *.tar.gz
12 |
13 | # logs
14 | targets.log
15 | dryrun.log
16 |
17 | # ides
18 | .idea/
19 | .vscode/
20 |
--------------------------------------------------------------------------------
/tests/terraform_tests/playbooks/play-vault-test.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: A vault test playbook
3 | hosts: localhost
4 |
5 | vars:
6 | docker_name: "{{ docker_container }}"
7 | tasks:
8 | - name: Hello there
9 | ansible.builtin.debug:
10 | msg:
11 | - "Hello there! The secret is:"
12 | - "{{ var_1 }}"
13 | - "{{ var_2 }}"
14 |
--------------------------------------------------------------------------------
/templates/index.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | page_title: "Ansible Provider"
3 | subcategory: ""
4 | description: |-
5 | Terraform provider for Ansible.
6 | ---
7 |
8 | # Ansible Provider
9 |
10 | The Ansible provider is used to interact with Ansible.
11 |
12 | Use the navigation to the left to read about the available resources.
13 |
14 |
15 | ## Example Usage
16 |
17 | {{ tffile .ExampleFile }}
18 |
--------------------------------------------------------------------------------
/templates/resources/vault.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_vault Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_vault (Resource)
10 |
11 | Provides an Ansible vault resource.
12 |
13 | ## Example Usage
14 | {{ tffile .ExampleFile }}
15 |
16 | {{ .SchemaMarkdown }}
17 |
--------------------------------------------------------------------------------
/templates/resources/host.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_host Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_host (Resource)
10 |
11 | Provides an Ansible inventory host resource.
12 |
13 | ## Example Usage
14 | {{ tffile .ExampleFile }}
15 |
16 | {{ .SchemaMarkdown }}
17 |
--------------------------------------------------------------------------------
/examples/actions/ansible_playbook_run/action.tf:
--------------------------------------------------------------------------------
1 | action "ansible_playbook_run" "ansible" {
2 | config {
3 | playbooks = ["${path.module}/playbook.yml"]
4 | inventory = [ansible_inventory.myinventory.path]
5 | ssh_private_key_file = "./ssh-private-key.pem"
6 |
7 | extra_vars = {
8 | var_a = "Some variable"
9 | var_b = "Another variable"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/templates/resources/group.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_group Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_group (Resource)
10 |
11 | Provides an Ansible inventory group resource.
12 |
13 | ## Example Usage
14 | {{ tffile .ExampleFile }}
15 |
16 | {{ .SchemaMarkdown }}
17 |
--------------------------------------------------------------------------------
/templates/resources/playbook.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_playbook Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_playbook (Resource)
10 |
11 | Provides an Ansible playbook resource.
12 |
13 | ## Example Usage
14 | {{ tffile .ExampleFile }}
15 |
16 | {{ .SchemaMarkdown }}
17 |
--------------------------------------------------------------------------------
/examples/aws/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Install nginx on remote host
3 | hosts: nginx
4 | become: true
5 | gather_facts: false
6 | tasks:
7 | - wait_for_connection:
8 |
9 | - setup:
10 |
11 | - name: Install nginx
12 | package:
13 | name: nginx
14 | state: present
15 |
16 | - name: Start nginx
17 | service:
18 | name: nginx
19 | state: started
20 |
--------------------------------------------------------------------------------
/.github/workflows/changelog.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Changelog
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | pull_request:
9 | types:
10 | - opened
11 | - reopened
12 | - labeled
13 | - unlabeled
14 | - synchronize
15 | branches:
16 | - main
17 | - stable-*
18 |
19 | jobs:
20 | changelog:
21 | uses: ansible-network/github_actions/.github/workflows/changelog.yml@main
22 |
--------------------------------------------------------------------------------
/provider/provider.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
5 | )
6 |
7 | // Provider exported function.
8 | func Provider() *schema.Provider {
9 | return &schema.Provider{
10 | ResourcesMap: map[string]*schema.Resource{
11 | "ansible_playbook": resourcePlaybook(),
12 | "ansible_vault": resourceVault(),
13 | "ansible_host": resourceHost(),
14 | "ansible_group": resourceGroup(),
15 | },
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/templates/actions/playbook_run.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_playbook_run Action - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |- Run an Ansible playbook.
6 | ---
7 |
8 | # ansible_playbook_run (Action)
9 |
10 | The `ansible_playbook_run` action runs an Ansible playbook.
11 |
12 | {{ if .HasExample -}}
13 | ## Example Usage
14 | {{ tffile .ExampleFile }}
15 | {{- end }}
16 |
17 | {{ .SchemaMarkdown }}
18 |
--------------------------------------------------------------------------------
/templates/data-sources/inventory.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_inventory DataSource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_inventory (DataSource)
10 |
11 | This data source represents an ansible inventory. It has a json attribute containing the JSON representation of the inventory.
12 |
13 | ## Example Usage
14 | {{ tffile .ExampleFile }}
15 |
16 | {{ .SchemaMarkdown }}
17 |
--------------------------------------------------------------------------------
/examples/resources/ansible_host/resource.tf:
--------------------------------------------------------------------------------
1 | resource "ansible_host" "host" {
2 | name = "somehost"
3 | groups = ["somegroup"]
4 |
5 | variables = {
6 | greetings = "from host!"
7 | some = "variable"
8 | yaml_hello = local.decoded_vault_yaml.hello
9 | yaml_number = local.decoded_vault_yaml.a_number
10 |
11 | # using jsonencode() here is needed to stringify
12 | # a list that looks like: [ element_1, element_2, ..., element_N ]
13 | yaml_list = jsonencode(local.decoded_vault_yaml.a_list)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/vault-file.yml:
--------------------------------------------------------------------------------
1 | $ANSIBLE_VAULT;1.1;AES256
2 | 63663264353833346631323435383339353261613436633737353739396466616263646531623231
3 | 6134363862383863363733656133386133656463623330300a363863303530656666623763303636
4 | 35343036343639633431366539323666653130633936643061343932346163653631313938333363
5 | 3566356437653131330a393734333335313539646363316339393861376166353963653136386235
6 | 39323531653537343734613934633866336533366236623131313438303836633935626262346230
7 | 62383161356666616366623762373665353834633534366531643961663338313765656430316562
8 | 336237616635653038333535303162613965
9 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | ansible = {
4 | source = "ansible/ansible"
5 | version = "~> 1.1.0"
6 | }
7 | docker = {
8 | source = "kreuzwerker/docker"
9 | version = "~> 3.0.1"
10 | }
11 | }
12 | }
13 |
14 | # ===============================================
15 | # Create a docker image using a Dockerfile
16 | # ===============================================
17 | resource "docker_image" "julia" {
18 | name = "julian-alps:latest"
19 | build {
20 | context = "."
21 | dockerfile = "Dockerfile"
22 | }
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | default: all
4 | disable:
5 | - cyclop
6 | - depguard
7 | - dupl
8 | - exhaustruct
9 | - funlen
10 | - gocognit
11 | - gocyclo
12 | - nestif
13 | - revive
14 | - rowserrcheck
15 | - sqlclosecheck
16 | - wastedassign
17 | exclusions:
18 | generated: lax
19 | presets:
20 | - comments
21 | - common-false-positives
22 | - legacy
23 | - std-error-handling
24 | paths:
25 | - third_party$
26 | - builtin$
27 | - examples$
28 | formatters:
29 | enable:
30 | - gofmt
31 | - gofumpt
32 | - goimports
33 | exclusions:
34 | generated: lax
35 | paths:
36 | - third_party$
37 | - builtin$
38 | - examples$
39 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/simple-playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # ++++++++++++++
3 | # Create an empty text file on a host
4 | # ++++++++++++++
5 | - hosts: all
6 | vars:
7 | simple_file: ~/simple-file.txt
8 | tags:
9 | - tag1
10 | tasks:
11 | - ansible.builtin.file:
12 | path: "{{ simple_file }}"
13 | state: touch
14 |
15 | # ++++++++++++++
16 | # Write into a text file on a host
17 | # ++++++++++++++
18 | - hosts: all
19 | vars:
20 | simple_file: ~/simple-file.txt
21 | tags:
22 | - tag2
23 | tasks:
24 | - ansible.builtin.copy:
25 | content: |
26 | Hello, World!
27 | {{ injected_variable }}
28 | {{ content_from_a_var_file }}
29 | {{ content_from_a_vault_file }}
30 | dest: "{{ simple_file }}"
31 |
--------------------------------------------------------------------------------
/tests/terraform_tests/run_tftest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | dir=$(pwd)
6 | tempdir="$(mktemp -d $dir/temp.XXXXXX)"
7 | export TF_CLI_CONFIG_FILE="$dir/ansible-dev.tfrc"
8 |
9 | function teardown()
10 | {
11 | rm -rf "$tempdir"
12 | }
13 |
14 | trap teardown EXIT
15 |
16 | cat vault-decrypted.yml > vault-encrypted.yml
17 | ansible-vault encrypt --vault-id testvault@vault_password vault-encrypted.yml
18 |
19 | cp -v main.tf $tempdir
20 | mv vault-encrypted.yml $tempdir
21 | cp vault_password $tempdir
22 |
23 | cd $tempdir
24 |
25 | terraform init || true # expected to fail
26 | terraform apply --auto-approve
27 | cat terraform.tfstate > ../actual_tfstate.json
28 |
29 | cd ../../integration
30 | set +e
31 | go test -v
32 | exit_code="$?"
33 | set -e
34 |
35 | exit "$exit_code"
36 |
--------------------------------------------------------------------------------
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Integration
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - stable-*
8 | pull_request:
9 |
10 | jobs:
11 | tests:
12 | name: Integration Tests
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out code
16 | uses: actions/checkout@v3
17 |
18 | - uses: hashicorp/setup-terraform@v3
19 | with:
20 | terraform_version: '1.14.0-beta1'
21 | terraform_wrapper: false
22 |
23 | - uses: actions/setup-python@v4
24 | with:
25 | python-version: '3.10'
26 | - run: |
27 | pip install ansible-core
28 |
29 | - uses: actions/setup-go@v5
30 | with:
31 | go-version-file: 'go.mod'
32 |
33 | - name: Run tests
34 | run: |
35 | make test
36 |
--------------------------------------------------------------------------------
/docs/resources/group.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_group Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_group (Resource)
10 |
11 | Provides an Ansible inventory group resource.
12 |
13 | ## Example Usage
14 | ```terraform
15 | resource "ansible_group" "group" {
16 | name = "somegroup"
17 | children = ["somechild"]
18 | variables = {
19 | hello = "from group!"
20 | }
21 | }
22 | ```
23 |
24 |
25 | ## Schema
26 |
27 | ### Required
28 |
29 | - `name` (String) Name of the group.
30 |
31 | ### Optional
32 |
33 | - `children` (List of String) List of group children.
34 | - `variables` (Map of String) Map of variables.
35 |
36 | ### Read-Only
37 |
38 | - `id` (String) The ID of this resource.
39 |
40 |
41 |
--------------------------------------------------------------------------------
/docs/resources/vault.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_vault Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_vault (Resource)
10 |
11 | Provides an Ansible vault resource.
12 |
13 | ## Example Usage
14 | ```terraform
15 | resource "ansible_vault" "secrets" {
16 | vault_file = "vault.yml"
17 | vault_password_file = "/path/to/file"
18 | }
19 | ```
20 |
21 |
22 | ## Schema
23 |
24 | ### Required
25 |
26 | - `vault_file` (String) Path to encrypted vault file.
27 | - `vault_password_file` (String) Path to vault password file.
28 |
29 | ### Optional
30 |
31 | - `vault_id` (String) ID of the encrypted vault file.
32 |
33 | ### Read-Only
34 |
35 | - `args` (List of String)
36 | - `id` (String) The ID of this resource.
37 | - `yaml` (String, Sensitive)
38 |
39 |
40 |
--------------------------------------------------------------------------------
/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 | ignore_other_fragment_extensions: true
6 | is_other_project: true
7 | keep_fragments: false
8 | mention_ancestor: true
9 | new_plugins_after_name: removed_features
10 | notesdir: fragments
11 | prelude_section_name: release_summary
12 | prelude_section_title: Release Summary
13 | sanitize_changelog: true
14 | sections:
15 | - - major_changes
16 | - Major Changes
17 | - - minor_changes
18 | - Minor Changes
19 | - - breaking_changes
20 | - Breaking Changes / Porting Guide
21 | - - deprecated_features
22 | - Deprecated Features
23 | - - removed_features
24 | - Removed Features (previously deprecated)
25 | - - security_fixes
26 | - Security Fixes
27 | - - bugfixes
28 | - Bugfixes
29 | - - known_issues
30 | - Known Issues
31 | title: The Terraform Provider for Ansible
32 | trivial_section_name: trivial
33 | use_semantic_versioning: true
34 |
--------------------------------------------------------------------------------
/.github/workflows/linters.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Linters
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - stable-*
8 | pull_request:
9 |
10 | jobs:
11 | linters:
12 | name: Linters
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out code
16 | uses: actions/checkout@v3
17 |
18 | - uses: actions/setup-go@v5
19 | with:
20 | go-version-file: 'go.mod'
21 | cache: false
22 |
23 | - name: golangci-lint
24 | uses: golangci/golangci-lint-action@v7
25 | with:
26 | # Require: The version of golangci-lint to use.
27 | version: v2.1.6
28 | # Optional: golangci-lint command line arguments.
29 | # Note: By default, the `.golangci.yml` file should be at the root of the repository.
30 | # The location of the configuration file can be changed by using `--config=`
31 | args: --timeout=10m --config=.golangci.yml
32 | # Optional: if set to true, then the action won't cache or restore ~/go/pkg.
33 | skip-pkg-cache: true
34 |
--------------------------------------------------------------------------------
/examples/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | ansible = {
4 | version = "~> 1.1.0"
5 | source = "ansible/ansible"
6 | }
7 | }
8 | }
9 |
10 |
11 | resource "ansible_vault" "secrets" {
12 | vault_file = "vault.yml"
13 | vault_password_file = "/path/to/file"
14 | }
15 |
16 |
17 | locals {
18 | decoded_vault_yaml = yamldecode(ansible_vault.secrets.yaml)
19 | }
20 |
21 | resource "ansible_host" "host" {
22 | name = "somehost"
23 | groups = ["somegroup"]
24 |
25 | variables = {
26 | greetings = "from host!"
27 | some = "variable"
28 | yaml_hello = local.decoded_vault_yaml.hello
29 | yaml_number = local.decoded_vault_yaml.a_number
30 |
31 | # using jsonencode() here is needed to stringify
32 | # a list that looks like: [ element_1, element_2, ..., element_N ]
33 | yaml_list = jsonencode(local.decoded_vault_yaml.a_list)
34 | }
35 | }
36 |
37 | resource "ansible_group" "group" {
38 | name = "somegroup"
39 | children = ["somechild"]
40 | variables = {
41 | hello = "from group!"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/examples/provider/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | ansible = {
4 | version = "~> 1.3.0"
5 | source = "ansible/ansible"
6 | }
7 | }
8 | }
9 |
10 |
11 | resource "ansible_vault" "secrets" {
12 | vault_file = "vault.yml"
13 | vault_password_file = "/path/to/file"
14 | }
15 |
16 |
17 | locals {
18 | decoded_vault_yaml = yamldecode(ansible_vault.secrets.yaml)
19 | }
20 |
21 | resource "ansible_host" "host" {
22 | name = "somehost"
23 | groups = ["somegroup"]
24 |
25 | variables = {
26 | greetings = "from host!"
27 | some = "variable"
28 | yaml_hello = local.decoded_vault_yaml.hello
29 | yaml_number = local.decoded_vault_yaml.a_number
30 |
31 | # using jsonencode() here is needed to stringify
32 | # a list that looks like: [ element_1, element_2, ..., element_N ]
33 | yaml_list = jsonencode(local.decoded_vault_yaml.a_list)
34 | }
35 | }
36 |
37 | resource "ansible_group" "group" {
38 | name = "somegroup"
39 | children = ["somechild"]
40 | variables = {
41 | hello = "from group!"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/terraform_tests/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | ansible = {
4 | version = "~> 1.0.0"
5 | source = "ansible/ansible"
6 | }
7 | }
8 | }
9 |
10 |
11 | resource "ansible_vault" "secrets" {
12 | # required options
13 | vault_file = "vault-encrypted.yml"
14 | vault_password_file = "vault_password"
15 |
16 | # optional options
17 | vault_id = "testvault"
18 | }
19 |
20 |
21 | locals {
22 | decoded_vault_yaml = yamldecode(ansible_vault.secrets.yaml)
23 | }
24 |
25 | resource "ansible_host" "host" {
26 | name = "somehost"
27 | groups = ["somegroup"]
28 |
29 | variables = {
30 | greetings = "from host!"
31 | some = "variable"
32 | yaml_hello = local.decoded_vault_yaml.hello
33 | yaml_number = local.decoded_vault_yaml.a_number
34 |
35 | # using jsonencode() here is needed to stringify
36 | # a list that looks like: [ element_1, element_2, ..., element_N ]
37 | yaml_list = jsonencode(local.decoded_vault_yaml.a_list)
38 | }
39 | }
40 |
41 | resource "ansible_group" "group" {
42 | name = "somegroup"
43 | children = ["somechild"]
44 | variables = {
45 | hello = "from group!"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/docs/resources/host.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_host Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_host (Resource)
10 |
11 | Provides an Ansible inventory host resource.
12 |
13 | ## Example Usage
14 | ```terraform
15 | resource "ansible_host" "host" {
16 | name = "somehost"
17 | groups = ["somegroup"]
18 |
19 | variables = {
20 | greetings = "from host!"
21 | some = "variable"
22 | yaml_hello = local.decoded_vault_yaml.hello
23 | yaml_number = local.decoded_vault_yaml.a_number
24 |
25 | # using jsonencode() here is needed to stringify
26 | # a list that looks like: [ element_1, element_2, ..., element_N ]
27 | yaml_list = jsonencode(local.decoded_vault_yaml.a_list)
28 | }
29 | }
30 | ```
31 |
32 |
33 | ## Schema
34 |
35 | ### Required
36 |
37 | - `name` (String) Name of the host.
38 |
39 | ### Optional
40 |
41 | - `groups` (List of String) List of group names.
42 | - `variables` (Map of String) Map of variables.
43 |
44 | ### Read-Only
45 |
46 | - `id` (String) The ID of this resource.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 |
7 | "github.com/ansible/terraform-provider-ansible/framework"
8 | "github.com/ansible/terraform-provider-ansible/provider"
9 | "github.com/hashicorp/terraform-plugin-framework/providerserver"
10 | "github.com/hashicorp/terraform-plugin-go/tfprotov5"
11 | "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server"
12 | "github.com/hashicorp/terraform-plugin-mux/tf5muxserver"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14 | )
15 |
16 | // Generate the Terraform provider documentation using `tfplugindocs`:
17 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
18 |
19 | func main() {
20 | ctx := context.Background()
21 | primary := provider.Provider()
22 | providers := []func() tfprotov5.ProviderServer{
23 | func() tfprotov5.ProviderServer {
24 | return schema.NewGRPCProviderServer(primary)
25 | },
26 | providerserver.NewProtocol5(framework.New(primary)),
27 | }
28 |
29 | muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...)
30 | if err != nil {
31 | log.Fatal(err)
32 | }
33 |
34 | var serveOpts []tf5server.ServeOpt
35 |
36 | err = tf5server.Serve("registry.terraform.io/ansible/ansible", muxServer.ProviderServer, serveOpts...)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/data-sources/ansible_inventory/data-source.tf:
--------------------------------------------------------------------------------
1 | data "ansible_inventory" "myinventory" {
2 | group {
3 | name = "webservers"
4 |
5 | host {
6 | name = aws_instance.web.public_ip
7 | ansible_user = "ubuntu"
8 | ansible_private_key_file = local_file.private_key.filename
9 | ansible_ssh_extra_args = "-o StrictHostKeyChecking=no"
10 | }
11 | }
12 |
13 | group {
14 | name = "dbservers"
15 |
16 | host {
17 | name = aws_instance.primary_db.public_ip
18 | ansible_user = "root"
19 | ansible_private_key_file = local_file.private_key.filename
20 | }
21 |
22 | host {
23 | name = aws_instance.fallback_db.public_ip
24 | ansible_user = "root"
25 | ansible_private_key_file = local_file.private_key.filename
26 | }
27 | }
28 | }
29 |
30 | # If you need the inventory as a file you can use the local_file resource
31 | resource "local_file" "myinventory" {
32 | content = ansible_inventory.myinventory.json
33 | filename = "${path.module}/inventory.json"
34 | }
35 |
36 | # It can also be used directly in an Action
37 | action "ansible_playbook_run" "ansible" {
38 | config {
39 | playbooks = ["${path.module}/playbook.yml"]
40 | inventories = [data.ansible_inventory.host.json]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ================================================
2 | The Terraform Provider for Ansible Release Notes
3 | ================================================
4 |
5 | .. contents:: Topics
6 |
7 | v1.3.0
8 | ======
9 |
10 | Minor Changes
11 | -------------
12 |
13 | - resource/ansible_playbook - Provider should failed with proper message when ansible is not installed (https://github.com/ansible/terraform-provider-ansible/issues/35).
14 |
15 | Bugfixes
16 | --------
17 |
18 | - ensure extra vars are quoted (https://github.com/ansible/terraform-provider-ansible/pull/57).
19 |
20 | v1.2.0
21 | ======
22 |
23 | Release Summary
24 | ---------------
25 |
26 | The terraform-provider-ansible v1.2.0 includes minor bugfixes and improvements.
27 |
28 | Minor Changes
29 | -------------
30 |
31 | - Update dependencies (google.golang.org/grpc and golang.org/x/net) to resolve security alerts https://github.com/ansible/terraform-provider-ansible/security/dependabot (https://github.com/ansible/terraform-provider-ansible/pull/72).
32 | - Updates the provider to use Go 1.21 (https://github.com/ansible/terraform-provider-ansible/pull/89)
33 | - Updates the provider to use SDKv2 (https://github.com/ansible/terraform-provider-ansible/issues/39).
34 |
35 | Bugfixes
36 | --------
37 |
38 | - provider/resource_playbook - Fix race condition between multiple ansible_playbook resources (https://github.com/ansible/terraform-provider-ansible/issues/38).
39 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_title: "Ansible Provider"
3 | subcategory: ""
4 | description: |-
5 | Terraform provider for Ansible.
6 | ---
7 |
8 | # Ansible Provider
9 |
10 | The Ansible provider is used to interact with Ansible.
11 |
12 | Use the navigation to the left to read about the available resources.
13 |
14 |
15 | ## Example Usage
16 |
17 | ```terraform
18 | terraform {
19 | required_providers {
20 | ansible = {
21 | version = "~> 1.3.0"
22 | source = "ansible/ansible"
23 | }
24 | }
25 | }
26 |
27 |
28 | resource "ansible_vault" "secrets" {
29 | vault_file = "vault.yml"
30 | vault_password_file = "/path/to/file"
31 | }
32 |
33 |
34 | locals {
35 | decoded_vault_yaml = yamldecode(ansible_vault.secrets.yaml)
36 | }
37 |
38 | resource "ansible_host" "host" {
39 | name = "somehost"
40 | groups = ["somegroup"]
41 |
42 | variables = {
43 | greetings = "from host!"
44 | some = "variable"
45 | yaml_hello = local.decoded_vault_yaml.hello
46 | yaml_number = local.decoded_vault_yaml.a_number
47 |
48 | # using jsonencode() here is needed to stringify
49 | # a list that looks like: [ element_1, element_2, ..., element_N ]
50 | yaml_list = jsonencode(local.decoded_vault_yaml.a_list)
51 | }
52 | }
53 |
54 | resource "ansible_group" "group" {
55 | name = "somegroup"
56 | children = ["somechild"]
57 | variables = {
58 | hello = "from group!"
59 | }
60 | }
61 | ```
62 |
--------------------------------------------------------------------------------
/tests/integration/provider_test.go:
--------------------------------------------------------------------------------
1 | // integration tests package
2 | package integration_tests_test
3 |
4 | import (
5 | "log"
6 | "testing"
7 |
8 | "github.com/Jeffail/gabs"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | const (
13 | actualJSON = "../terraform_tests/actual_tfstate.json"
14 | expectedJSON = "../expected_tfstate.json"
15 | )
16 |
17 | func TestAnsibleProviderOutputs(t *testing.T) {
18 | t.Parallel()
19 |
20 | actual, errAct := gabs.ParseJSONFile(actualJSON)
21 | expected, errExp := gabs.ParseJSONFile(expectedJSON)
22 |
23 | // "serial" is a changing variable (it changes after
24 | // every 'terraform destroy'), so we're not testing that.
25 | if _, err := expected.Set(actual.Path("serial").Data(), "serial"); err != nil {
26 | log.Fatalf("Error: couldn't ignore 'serial' field! %s", err)
27 | }
28 |
29 | // "lineage" is a changing variable (it is dependent on the
30 | // terraform working directory), so we're not testing that.
31 | if _, err := expected.Set(actual.Path("lineage").Data(), "lineage"); err != nil {
32 | log.Fatalf("Error: couldn't ignore 'lineage' field! %s", err)
33 | }
34 |
35 | if errAct != nil {
36 | log.Fatal("Error in " + actualJSON + "!")
37 | }
38 |
39 | if errExp != nil {
40 | log.Fatal("Error in " + expectedJSON + "!")
41 | }
42 |
43 | assert.JSONEq(t,
44 | expected.Path("resources").String(),
45 | actual.Path("resources").String(),
46 | "Actual and Expected JSON files don't match!")
47 | }
48 |
--------------------------------------------------------------------------------
/changelogs/changelog.yaml:
--------------------------------------------------------------------------------
1 | ancestor: null
2 | releases:
3 | 1.2.0:
4 | changes:
5 | bugfixes:
6 | - provider/resource_playbook - Fix race condition between multiple ansible_playbook
7 | resources (https://github.com/ansible/terraform-provider-ansible/issues/38).
8 | minor_changes:
9 | - Update dependencies (google.golang.org/grpc and golang.org/x/net) to resolve
10 | security alerts https://github.com/ansible/terraform-provider-ansible/security/dependabot
11 | (https://github.com/ansible/terraform-provider-ansible/pull/72).
12 | - Updates the provider to use Go 1.21 (https://github.com/ansible/terraform-provider-ansible/pull/89)
13 | - Updates the provider to use SDKv2 (https://github.com/ansible/terraform-provider-ansible/issues/39).
14 | release_summary: The terraform-provider-ansible v1.2.0 includes minor bugfixes
15 | and improvements.
16 | fragments:
17 | - aws_example.yml
18 | - go_version_updates.yml
19 | - gplv3_licensing.yml
20 | - inventory_race_conditions.yml
21 | - release-summary.yml
22 | - update_dependencies.yml
23 | - use_sdkv2.yml
24 | release_date: '2024-02-21'
25 | 1.3.0:
26 | changes:
27 | bugfixes:
28 | - ensure extra vars are quoted (https://github.com/ansible/terraform-provider-ansible/pull/57).
29 | minor_changes:
30 | - resource/ansible_playbook - Provider should failed with proper message when
31 | ansible is not installed (https://github.com/ansible/terraform-provider-ansible/issues/35).
32 | fragments:
33 | - 20240410-proper-failure-message-when-ansible-is-not-installed.yml
34 | - quote-extra-vars.yml
35 | - update_dependencies.yml
36 | release_date: '2024-05-07'
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This GitHub action can publish assets for release when a tag is created.
3 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0).
4 | #
5 | # This uses an action (crazy-max/ghaction-import-gpg) that assumes you set your
6 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE`
7 | # secret. If you would rather own your own GPG handling, please fork this action
8 | # or use an alternative one for key handling.
9 | #
10 | # You will need to pass the `--batch` flag to `gpg` in your signing step
11 | # in `goreleaser` to indicate this is being used in a non-interactive mode.
12 | #
13 | name: release
14 | on:
15 | push:
16 | tags:
17 | - 'v*'
18 | permissions:
19 | contents: write
20 | jobs:
21 | goreleaser:
22 | runs-on: ubuntu-latest
23 | steps:
24 | -
25 | name: Checkout
26 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
27 | -
28 | name: Unshallow
29 | run: git fetch --prune --unshallow
30 | -
31 | name: Set up Go
32 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
33 | with:
34 | go-version-file: 'go.mod'
35 | cache: true
36 | -
37 | name: Import GPG key
38 | uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0
39 | id: import_gpg
40 | with:
41 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
42 | passphrase: ${{ secrets.PASSPHRASE }}
43 | -
44 | name: Run GoReleaser
45 | uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0
46 | with:
47 | version: latest
48 | args: release --rm-dist
49 | env:
50 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
51 | # GitHub sets this automatically
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 |
--------------------------------------------------------------------------------
/provider/resource_host.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | )
9 |
10 | func resourceHost() *schema.Resource {
11 | return &schema.Resource{
12 | CreateContext: resourceHostCreate,
13 | ReadContext: resourceHostRead,
14 | UpdateContext: resourceHostUpdate,
15 | DeleteContext: resourceHostDelete,
16 |
17 | Schema: map[string]*schema.Schema{
18 | "name": {
19 | Type: schema.TypeString,
20 | Required: true,
21 | Optional: false,
22 | Description: "Name of the host.",
23 | },
24 | "groups": {
25 | Type: schema.TypeList,
26 | Required: false,
27 | Optional: true,
28 | Elem: &schema.Schema{
29 | Type: schema.TypeString,
30 | },
31 | Description: "List of group names.",
32 | },
33 | "variables": {
34 | Type: schema.TypeMap,
35 | Required: false,
36 | Optional: true,
37 | Elem: &schema.Schema{Type: schema.TypeString},
38 | Description: "Map of variables.",
39 | },
40 | },
41 | }
42 | }
43 |
44 | func resourceHostCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
45 | var diags diag.Diagnostics
46 |
47 | hostName, ok := data.Get("name").(string)
48 |
49 | if !ok {
50 | diags = append(diags, diag.Diagnostic{
51 | Severity: diag.Error,
52 | Summary: "ERROR [ansible-group]: couldn't get 'name'!",
53 | })
54 | }
55 |
56 | data.SetId(hostName)
57 |
58 | resourceHostRead(ctx, data, meta)
59 |
60 | return diags
61 | }
62 |
63 | func resourceHostRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
64 | return nil
65 | }
66 |
67 | func resourceHostUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
68 | return resourceHostRead(ctx, data, meta)
69 | }
70 |
71 | func resourceHostDelete(_ context.Context, data *schema.ResourceData, _ interface{}) diag.Diagnostics {
72 | data.SetId("")
73 |
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/provider/resource_group.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | )
9 |
10 | func resourceGroup() *schema.Resource {
11 | return &schema.Resource{
12 | CreateContext: resourceGroupCreate,
13 | ReadContext: resourceGroupRead,
14 | UpdateContext: resourceGroupUpdate,
15 | DeleteContext: resourceGroupDelete,
16 |
17 | Schema: map[string]*schema.Schema{
18 | "name": {
19 | Type: schema.TypeString,
20 | Required: true,
21 | Optional: false,
22 | Description: "Name of the group.",
23 | },
24 | "children": {
25 | Type: schema.TypeList,
26 | Required: false,
27 | Optional: true,
28 | Elem: &schema.Schema{
29 | Type: schema.TypeString,
30 | },
31 | Description: "List of group children.",
32 | },
33 | "variables": {
34 | Type: schema.TypeMap,
35 | Required: false,
36 | Optional: true,
37 | Elem: &schema.Schema{Type: schema.TypeString},
38 | Description: "Map of variables.",
39 | },
40 | },
41 | }
42 | }
43 |
44 | func resourceGroupCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
45 | var diags diag.Diagnostics
46 |
47 | groupName, ok := data.Get("name").(string)
48 |
49 | if !ok {
50 | diags = append(diags, diag.Diagnostic{
51 | Severity: diag.Error,
52 | Summary: "ERROR [ansible-group]: couldn't get 'name'!",
53 | })
54 | }
55 |
56 | data.SetId(groupName)
57 |
58 | resourceGroupRead(ctx, data, meta)
59 |
60 | return diags
61 | }
62 |
63 | func resourceGroupRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
64 | return nil
65 | }
66 |
67 | func resourceGroupUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
68 | return resourceGroupRead(ctx, data, meta)
69 | }
70 |
71 | func resourceGroupDelete(_ context.Context, data *schema.ResourceData, _ interface{}) diag.Diagnostics {
72 | data.SetId("")
73 |
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 |
4 | # Visit https://goreleaser.com for documentation on how to customize this
5 | # behavior.
6 | before:
7 | hooks:
8 | # this is just an example and not a requirement for provider building/publishing
9 | - go mod tidy
10 | builds:
11 | - env:
12 | # goreleaser does not work with CGO, it could also complicate
13 | # usage by users in CI/CD systems like Terraform Cloud where
14 | # they are unable to install libraries.
15 | - CGO_ENABLED=0
16 | mod_timestamp: '{{ .CommitTimestamp }}'
17 | flags:
18 | - -trimpath
19 | ldflags:
20 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
21 | goos:
22 | - freebsd
23 | - windows
24 | - linux
25 | - darwin
26 | goarch:
27 | - amd64
28 | - '386'
29 | - arm
30 | - arm64
31 | ignore:
32 | - goos: darwin
33 | goarch: '386'
34 | binary: '{{ .ProjectName }}_v{{ .Version }}'
35 | archives:
36 | - format: zip
37 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
38 | checksum:
39 | extra_files:
40 | - glob: 'terraform-registry-manifest.json'
41 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
42 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
43 | algorithm: sha256
44 | signs:
45 | - artifacts: checksum
46 | args:
47 | # if you are using this in a GitHub action or some other automated pipeline, you
48 | # need to pass the batch flag to indicate its not interactive.
49 | - "--batch"
50 | - "--local-user"
51 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
52 | - "--output"
53 | - "${signature}"
54 | - "--detach-sign"
55 | - "${artifact}"
56 | release:
57 | extra_files:
58 | - glob: 'terraform-registry-manifest.json'
59 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
60 | # If you want to manually examine the release before its live, uncomment this line:
61 | # draft: true
62 | changelog:
63 | skip: true
64 |
--------------------------------------------------------------------------------
/framework/provider.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/hashicorp/terraform-plugin-framework/action"
7 | "github.com/hashicorp/terraform-plugin-framework/datasource"
8 | "github.com/hashicorp/terraform-plugin-framework/provider"
9 | "github.com/hashicorp/terraform-plugin-framework/provider/schema"
10 | "github.com/hashicorp/terraform-plugin-framework/resource"
11 | )
12 |
13 | var _ provider.Provider = &fwprovider{}
14 |
15 | // New returns a new, initialized Terraform Plugin Framework-style provider instance.
16 | // The provider instance is fully configured once the `Configure` method has been called.
17 | func New(primary interface{ Meta() interface{} }) provider.Provider {
18 | return &fwprovider{
19 | Primary: primary,
20 | }
21 | }
22 |
23 | type fwprovider struct {
24 | Primary interface{ Meta() interface{} }
25 | }
26 |
27 | func (f *fwprovider) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) {
28 | response.TypeName = "ansible"
29 | }
30 |
31 | func (f *fwprovider) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) {
32 | response.Schema = schema.Schema{
33 | Attributes: map[string]schema.Attribute{},
34 | Blocks: map[string]schema.Block{},
35 | }
36 | }
37 |
38 | func (f *fwprovider) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) {
39 | // Provider's parsed configuration (its instance state) is available through the primary provider's Meta() method.
40 | v := f.Primary.Meta()
41 | response.DataSourceData = v
42 | response.ResourceData = v
43 | response.EphemeralResourceData = v
44 | response.ActionData = v
45 | }
46 |
47 | func (f *fwprovider) DataSources(ctx context.Context) []func() datasource.DataSource {
48 | return []func() datasource.DataSource{
49 | NewInventoryDataSource,
50 | }
51 | }
52 |
53 | func (f *fwprovider) Resources(ctx context.Context) []func() resource.Resource {
54 | return nil
55 | }
56 | func (p *fwprovider) Actions(ctx context.Context) []func() action.Action {
57 | return []func() action.Action{
58 | NewRunPlaybookRunAction,
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/aws/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | ansible = {
4 | version = "~> 1.1.0"
5 | source = "ansible/ansible"
6 | }
7 | aws = {
8 | source = "hashicorp/aws"
9 | version = "~> 4.0"
10 | }
11 | }
12 | }
13 |
14 | # Configure the AWS Provider
15 | provider "aws" {
16 | region = "eu-north-1"
17 | access_key = "my_acces_key"
18 | secret_key = "my_secret_key"
19 | }
20 |
21 | # Add key for ssh connection
22 | resource "aws_key_pair" "my_key" {
23 | key_name = "my_key"
24 | public_key = "my_public_key_value"
25 | }
26 |
27 | # Add security group for ssh
28 | resource "aws_security_group" "ssh" {
29 | name = "ssh"
30 | ingress {
31 | description = "ssh"
32 | from_port = 22
33 | to_port = 22
34 | protocol = "tcp"
35 | cidr_blocks = ["0.0.0.0/0"]
36 | }
37 | }
38 |
39 | # Add security group for http
40 | resource "aws_security_group" "http" {
41 | name = "http"
42 | ingress {
43 | description = "http"
44 | from_port = 80
45 | to_port = 80
46 | protocol = "tcp"
47 | cidr_blocks = ["0.0.0.0/0"]
48 | }
49 | }
50 |
51 | # Set ami for ec2 instance
52 | data "aws_ami" "ubuntu" {
53 | most_recent = true
54 | filter {
55 | name = "name"
56 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
57 | }
58 | filter {
59 | name = "virtualization-type"
60 | values = ["hvm"]
61 | }
62 | owners = ["099720109477"]
63 | }
64 |
65 | # Create ec2 instance
66 | resource "aws_instance" "my_ec2" {
67 | ami = data.aws_ami.ubuntu.id
68 | instance_type = "t3.micro"
69 | tags = {
70 | Name = "Inventory_plugin"
71 | }
72 | key_name = aws_key_pair.my_key.key_name
73 | security_groups = [aws_security_group.ssh.name, aws_security_group.http.name, "default"]
74 | }
75 |
76 | # Add created ec2 instance to ansible inventory
77 | resource "ansible_host" "my_ec2" {
78 | name = aws_instance.my_ec2.public_dns
79 | groups = ["nginx"]
80 | variables = {
81 | ansible_user = "ubuntu",
82 | ansible_ssh_private_key_file = "~/.ssh/id_rsa",
83 | ansible_python_interpreter = "/usr/bin/python3",
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/end-to-end-playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: all
3 | vars:
4 | tags:
5 | - tag1
6 | tasks:
7 | - ansible.builtin.lineinfile:
8 | path: "{{ test_filename }}"
9 | line: |-
10 | ----------
11 | {{ test_filename }}
12 | i have executed in tag1!
13 | {{ injected_variable | default("var not injected") }}
14 | {{ content_from_a_var_file | default("var file not specified") }}
15 | {{ content_from_a_vault_file | default("vault file not specified") }}
16 | ----------
17 | create: true
18 |
19 | - hosts: all
20 | vars:
21 | tags:
22 | - tag2
23 | tasks:
24 | - ansible.builtin.lineinfile:
25 | path: "{{ test_filename }}"
26 | line: |-
27 | ----------
28 | {{ test_filename }}
29 | i have executed in tag2!
30 | {{ injected_variable | default("var not injected") }}
31 | {{ content_from_a_var_file | default("var file not specified") }}
32 | {{ content_from_a_vault_file | default("vault file not specified") }}
33 | ----------
34 | create: true
35 |
36 | - hosts: this_group_exists
37 | tasks:
38 | - ansible.builtin.lineinfile:
39 | path: "{{ test_filename }}"
40 | line: |-
41 | ----------
42 | {{ test_filename }}
43 | i have executed in a group!
44 | {{ injected_variable | default("var not injected") }}
45 | {{ content_from_a_var_file | default("var file not specified") }}
46 | {{ content_from_a_vault_file | default("vault file not specified") }}
47 | ----------
48 | create: true
49 |
50 |
51 | - hosts: all
52 | tags:
53 | - tag_never_specified
54 | tasks:
55 | - ansible.builtin.lineinfile:
56 | path: "{{ test_filename }}"
57 | line: |-
58 | ----------
59 | {{ test_filename }}
60 | SHOULD EXECUTE IF NO TAG SPECIFIED: TAG NEVER SPECIFIED
61 | create: true
62 |
63 | - hosts: idonotexist
64 | tasks:
65 | - ansible.builtin.lineinfile:
66 | path: "{{ test_filename }}"
67 | line: |-
68 | -----------
69 | {{ test_filename }}
70 | SHOULD NEVER EXECUTE: HOST/GROUP NOT IN INVENTORY
71 | create: true
72 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/end-to-end-expected-output:
--------------------------------------------------------------------------------
1 | ----------
2 | test_e2e_groups.txt
3 | i have executed in tag1!
4 | var not injected
5 | var file not specified
6 | vault file not specified
7 | ----------
8 | ----------
9 | test_e2e_groups.txt
10 | i have executed in tag2!
11 | var not injected
12 | var file not specified
13 | vault file not specified
14 | ----------
15 | ----------
16 | test_e2e_groups.txt
17 | i have executed in a group!
18 | var not injected
19 | var file not specified
20 | vault file not specified
21 | ----------
22 | ----------
23 | test_e2e_groups.txt
24 | SHOULD EXECUTE IF NO TAG SPECIFIED: TAG NEVER SPECIFIED
25 | ----------
26 | test_e2e_limit_positive.txt
27 | i have executed in tag1!
28 | var not injected
29 | var file not specified
30 | vault file not specified
31 | ----------
32 | ----------
33 | test_e2e_limit_positive.txt
34 | i have executed in tag2!
35 | var not injected
36 | var file not specified
37 | vault file not specified
38 | ----------
39 | ----------
40 | test_e2e_limit_positive.txt
41 | SHOULD EXECUTE IF NO TAG SPECIFIED: TAG NEVER SPECIFIED
42 | ----------
43 | test_e2e_tags.txt
44 | i have executed in tag1!
45 | var not injected
46 | var file not specified
47 | vault file not specified
48 | ----------
49 | ----------
50 | test_e2e_tags.txt
51 | i have executed in tag2!
52 | var not injected
53 | var file not specified
54 | vault file not specified
55 | ----------
56 | ----------
57 | test_e2e_tags_1.txt
58 | i have executed in tag1!
59 | var not injected
60 | var file not specified
61 | vault file not specified
62 | ----------
63 | ----------
64 | test_e2e_tags_2.txt
65 | i have executed in tag2!
66 | var not injected
67 | var file not specified
68 | vault file not specified
69 | ----------
70 | ----------
71 | test_e2e_vars.txt
72 | i have executed in tag1!
73 | content
74 | content from a var file
75 | vault file not specified
76 | ----------
77 | ----------
78 | test_e2e_vars.txt
79 | i have executed in tag2!
80 | content
81 | content from a var file
82 | vault file not specified
83 | ----------
84 | ----------
85 | test_e2e_vars.txt
86 | SHOULD EXECUTE IF NO TAG SPECIFIED: TAG NEVER SPECIFIED
87 | ----------
88 | test_e2e_vault.txt
89 | i have executed in tag1!
90 | var not injected
91 | var file not specified
92 | content from a vault file
93 | ----------
94 | ----------
95 | test_e2e_vault.txt
96 | i have executed in tag2!
97 | var not injected
98 | var file not specified
99 | content from a vault file
100 | ----------
101 | ----------
102 | test_e2e_vault.txt
103 | SHOULD EXECUTE IF NO TAG SPECIFIED: TAG NEVER SPECIFIED
104 |
--------------------------------------------------------------------------------
/docs/actions/playbook_run.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_playbook_run Action - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |- Run an Ansible playbook.
6 | ---
7 |
8 | # ansible_playbook_run (Action)
9 |
10 | The `ansible_playbook_run` action runs an Ansible playbook.
11 |
12 | ## Example Usage
13 | ```terraform
14 | action "ansible_playbook_run" "ansible" {
15 | config {
16 | playbooks = ["${path.module}/playbook.yml"]
17 | inventory = [ansible_inventory.myinventory.path]
18 | ssh_private_key_file = "./ssh-private-key.pem"
19 |
20 | extra_vars = {
21 | var_a = "Some variable"
22 | var_b = "Another variable"
23 | }
24 | }
25 | }
26 | ```
27 |
28 |
29 | ## Schema
30 |
31 | ### Required
32 |
33 | - `playbooks` (List of String) Paths to ansible playbooks.
34 |
35 | ### Optional
36 |
37 | - `ansible_playbook_binary` (String) Path to ansible-playbook executable (binary).
38 | - `become` (Boolean) Run operations with become
39 | - `become_method` (String) Privilege escalation method to use (default=sudo), use `ansible-doc -t become -l` to list valid choices.
40 | - `become_password_file` (String) Path to file containing password for privilege escalation.
41 | - `become_user` (String) Become this user (default=root)
42 | - `check_mode` (Boolean) Run in check mode
43 | - `connection_password_file` (String) Path to file containing password for connection.
44 | - `connection_type` (String) Connection type to use (default=ssh)
45 | - `diff_mode` (Boolean) Run in diff mode
46 | - `extra_vars` (Map of String) Extra variables to pass to the playbook
47 | - `extra_vars_files` (List of String) List of variable files with extra variables
48 | - `flush_cache` (Boolean) Flush the cache before running the playbook.
49 | - `force_handlers` (Boolean) Force handlers to run even if a task fails.
50 | - `forks` (Number) Number of parallel forks to use
51 | - `inventories` (List of String) List of inventories in JSON format (use ansible_inventory to generate)
52 | - `inventory_files` (List of String) Specify inventory host path or comma separated host list
53 | - `limit` (String) Limit the execution to hosts matching a pattern
54 | - `module_paths` (List of String) Prepend path(s) to module library
55 | - `private_key_file` (String) Path to private key file
56 | - `quiet` (Boolean) Suppress output completely
57 | - `scp_extra_args` (String) Extra arguments to pass to scp
58 | - `sftp_extra_args` (String) Extra arguments to pass to sftp
59 | - `skip_tags` (List of String) List of tags to skip during playbook execution.
60 | - `ssh_common_args` (String) Specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand)
61 | - `ssh_extra_args` (String) Extra arguments to pass to ssh
62 | - `start_at_task` (String) Name of task to start execution at.
63 | - `tags` (List of String) Limit the execution to tasks matching a tag
64 | - `timeout` (Number) Override the connection timeout in seconds
65 | - `user` (String) Connect as this user (default=None)
66 | - `vault_ids` (List of String) The vault identities to use
67 | - `vault_password_file` (String) The vault password file to use
68 | - `verbosity` (Number) Verbosity level
69 |
70 |
71 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/simple.tf:
--------------------------------------------------------------------------------
1 | # ===============================================
2 | # Create docker containers to use as our hosts
3 | # ===============================================
4 | resource "docker_container" "julia_the_first" {
5 | image = docker_image.julia.image_id
6 | name = "julia-the-first"
7 | must_run = true
8 |
9 | # Make sure that this docker doesn't stop running
10 | command = [
11 | "sleep",
12 | "infinity"
13 | ]
14 | }
15 |
16 | resource "docker_container" "julia_the_second" {
17 | image = docker_image.julia.image_id
18 | name = "julia-the-second"
19 | must_run = true
20 |
21 | # Make sure that this docker doesn't stop running
22 | command = [
23 | "sleep",
24 | "infinity"
25 | ]
26 | }
27 |
28 | # ===============================================
29 | # Now set up our ansible_playbook resources
30 | # ===============================================
31 | resource "ansible_playbook" "example" {
32 | ansible_playbook_binary = "ansible-playbook" # this parameter is optional, default is "ansible-playbook"
33 | playbook = "simple-playbook.yml"
34 |
35 | # Inventory configuration
36 | name = docker_container.julia_the_first.name # name of the host to use for inventory configuration
37 | groups = ["playbook-group-1", "playbook-group-2"] # list of groups to add our host to
38 |
39 | # Ansible vault
40 | # you may also specify "vault_id" if it was set to the desired vault
41 | vault_password_file = "vault-password-file.txt"
42 | vault_files = [
43 | "vault-file.yml",
44 | ]
45 |
46 | # Play control
47 | # Configure our playbook execution, to run only tasks with specified tags.
48 | # in this example, we have only one tag; "tag1".
49 | tags = [
50 | "tag1"
51 | ]
52 |
53 | # Limit this playbook to run only on the host named "julia-the-first"
54 | limit = [
55 | docker_container.julia_the_first.name
56 | ]
57 | check_mode = false
58 | diff_mode = false
59 | var_files = [
60 | "var-file.yml"
61 | ]
62 |
63 | # Connection configuration and other vars
64 | extra_vars = {
65 | ansible_hostname = docker_container.julia_the_first.name
66 | ansible_connection = "docker"
67 | }
68 |
69 | replayable = true
70 | verbosity = 3 # set the verbosity level of the debug output for this playbook
71 | }
72 |
73 | resource "ansible_playbook" "example_2" {
74 | playbook = "simple-playbook.yml"
75 |
76 | # inventory configuration
77 | name = docker_container.julia_the_second.name
78 | groups = ["playbook-group-2"]
79 |
80 | # ansible vault
81 | vault_password_file = "vault-password-file.txt"
82 | vault_files = [
83 | "vault-file.yml",
84 | ]
85 |
86 | # play control
87 | tags = [
88 | "tag2"
89 | ]
90 | limit = [
91 | docker_container.julia_the_second.name
92 | ]
93 | check_mode = false
94 | diff_mode = false
95 | var_files = [
96 | "var-file.yml"
97 | ]
98 |
99 | # connection configuration and other vars
100 | extra_vars = {
101 | ansible_hostname = docker_container.julia_the_second.name
102 | ansible_connection = "docker"
103 | injected_variable = "Hello from simple.tf!"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/docs/resources/playbook.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_playbook Resource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_playbook (Resource)
10 |
11 | Provides an Ansible playbook resource.
12 |
13 | ## Example Usage
14 | ```terraform
15 | resource "ansible_playbook" "playbook" {
16 | playbook = "playbook.yml"
17 | name = "host-1.example.com"
18 | replayable = true
19 |
20 | extra_vars = {
21 | var_a = "Some variable"
22 | var_b = "Another variable"
23 | }
24 | }
25 | ```
26 |
27 |
28 | ## Schema
29 |
30 | ### Required
31 |
32 | - `name` (String) Name of the desired host on which the playbook will be executed.
33 | - `playbook` (String) Path to ansible playbook.
34 |
35 | ### Optional
36 |
37 | - `ansible_playbook_binary` (String) Path to ansible-playbook executable (binary).
38 | - `check_mode` (Boolean) If 'true', playbook execution won't make any changes but only change predictions will be made.
39 | - `diff_mode` (Boolean) If 'true', when changing (small) files and templates, differences in those files will be shown. Recommended usage with 'check_mode'.
40 | - `extra_vars` (Map of String) A map of additional variables as: { key-1 = value-1, key-2 = value-2, ... }.
41 | - `force_handlers` (Boolean) If 'true', run handlers even if a task fails.
42 | - `groups` (List of String) List of desired groups of hosts on which the playbook will be executed.
43 | - `ignore_playbook_failure` (Boolean) This parameter is good for testing. Set to 'true' if the desired playbook is meant to fail, but still want the resource to run successfully.
44 | - `limit` (List of String) List of hosts to include in playbook execution.
45 | - `replayable` (Boolean) If 'true', the playbook will be executed on every 'terraform apply' and with that, the resource will be recreated. If 'false', the playbook will be executed only on the first 'terraform apply'. Note, that if set to 'true', when doing 'terraform destroy', it might not show in the destroy output, even though the resource still gets destroyed.
46 | - `tags` (List of String) List of tags of plays and tasks to run.
47 | - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
48 | - `var_files` (List of String) List of variable files.
49 | - `vault_files` (List of String) List of vault files.
50 | - `vault_id` (String) ID of the desired vault(s).
51 | - `vault_password_file` (String) Path to a vault password file.
52 | - `verbosity` (Number) A verbosity level between 0 and 6. Set ansible 'verbose' parameter, which causes Ansible to print more debug messages. The higher the 'verbosity', the more debug details will be printed.
53 |
54 | ### Read-Only
55 |
56 | - `ansible_playbook_stderr` (String) An ansible-playbook CLI stderr output.
57 | - `ansible_playbook_stdout` (String) An ansible-playbook CLI stdout output.
58 | - `args` (List of String) Used to build arguments to run Ansible playbook with.
59 | - `id` (String) The ID of this resource.
60 | - `temp_inventory_file` (String) Path to created temporary inventory file.
61 |
62 |
63 | ### Nested Schema for `timeouts`
64 |
65 | Optional:
66 |
67 | - `create` (String)
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/README.md:
--------------------------------------------------------------------------------
1 | ## Run the examples
2 |
3 | **NOTE:** to run this example, you must have installed the following packages:
4 |
5 | | Name | Version used for testing |
6 | |-----------:|:--------------------------|
7 | | Terraform | v1.4.2 |
8 | | Python | 3.10.6 |
9 | | Docker | 23.0.1, build a5ee5b1 |
10 | | Golang | go1.18.10 linux/amd64 |
11 | | Ansible | core 2.14.3 |
12 |
13 | ------------------------------
14 |
15 | In base directory of this project ``terraform-provider-ansible/`` run (if not already):
16 | ```shell
17 | make build-dev
18 | ```
19 |
20 | In ``terraform-provider-ansible/examples/ansible_playbook`` directory
21 | ```shell
22 | terraform init
23 | terraform apply
24 |
25 | # For terraform debug mode, use:
26 | env TF_LOG=TRACE terraform apply
27 |
28 | # To destroy terraform instance:
29 | terraform destroy
30 | ```
31 | ------
32 | **NOTE**: if ``terraform destroy`` for whatever reason fails, you can manually destroy it by
33 | deleting all terraform related files in this directory. You may also need to [manually stop/remove all
34 | created dockers](#to-delete-all-created-dockers).
35 |
36 | ------
37 | ## How to check if everything works correctly
38 | Upon running ``terraform apply``, there should be three dockers generated:
39 | - By a simple example``simple.tf``:
40 | - ``julia-the-first``
41 | - ``julia-the-second``
42 | - By an end-to-end test ``end-to-end.tf``:
43 | - ``julia``
44 |
45 | To check this, use:
46 | ```shell
47 | docker ps
48 | ```
49 |
50 | To connect to the julia dockers, use:
51 | ```shell
52 | docker exec -it /bin/sh
53 | ```
54 |
55 | ### Quick links:
56 | 1. [Check the succession of end-to-end tests](#expected-results-for-the-end-to-end-tests)
57 | 2. [Check the succession of the simple example](#expected-results-for-the-simple-example)
58 |
59 | ## Expected results for the end-to-end tests
60 | On the ``julia`` docker, there should be 7 text files with a prefix ``test_e2e``, one for each ``e2e`` resource in
61 | ``end-to-end.tf`` (excluding ``e2e_limit_negative``, which should fail to run).
62 |
63 | To check if the generated results match the expected, use:
64 | ```shell
65 | # Save output of these files (sort the files alphabetically to make sure the output is always the same)
66 | docker exec -it julia sh -c 'ls -X /test_e2e* | xargs cat' > end-to-end-actual-output
67 |
68 | # Check diff of these files
69 | diff end-to-end-expected-output end-to-end-actual-output
70 | # if diff returns nothing, there are no differences.
71 | ```
72 |
73 | ## Expected results for the simple example
74 | On both dockers, there should be a text file ``~/simple-file.txt``.
75 | On ``julia-the-second``, this file should contain content of some variables from vaults and var files. Those variables are:
76 | - ``content_from_a_vault_file`` → from ``./vault-file.yml``
77 | - ``content_from_a_var_file`` → from ``./var-file.yml``
78 |
79 | The content of ``~/simple-file.txt`` should be something like this:
80 | ```
81 | Hello, World!
82 | Hello
83 | content from a var file
84 | content from a vault file
85 | ```
86 |
87 | On ``julia-the-first``, this file (``~/simple-file.txt``) should have no content.
88 |
89 | ## How to build a docker from Dockerfile manually (no Terraform)
90 | In ``terraform-provider-ansible/examples/ansible_playbook``:
91 | ```shell
92 | docker build -t docker_image .
93 | docker run --rm -d --name docker_name docker_image sleep infinity
94 | ```
95 |
96 | ### To delete all created dockers
97 | ```shell
98 | docker stop $(docker ps -a -q) && docker container prune
99 | ```
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Terraform Provider for Ansible
2 |
3 | The Terraform Provider for Ansible provides a more straightforward and robust means of executing Ansible automation from Terraform than local-exec. Paired with the inventory plugin in [the Ansible cloud.terraform collection](https://github.com/ansible-collections/cloud.terraform), users can run Ansible playbooks and roles on infrastructure provisioned by Terraform. The provider also includes integrated ansible-vault support.
4 |
5 | This provider can be [found in the Terraform Registry here](https://registry.terraform.io/providers/ansible/ansible/latest).
6 |
7 | For more details on using Terraform and Ansible together see these blog posts:
8 |
9 | * [Terraforming clouds with Ansible](https://www.ansible.com/blog/terraforming-clouds-with-ansible)
10 | * [Walking on Clouds with Ansible](https://www.ansible.com/blog/walking-on-clouds-with-ansible)
11 | * [Providing Terraform with that Ansible Magic](https://www.ansible.com/blog/providing-terraform-with-that-ansible-magic)
12 |
13 |
14 | ## Requirements
15 |
16 | - install Go: [official installation guide](https://go.dev/doc/install)
17 | - install Terraform: [official installation guide](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)
18 | - install Ansible: [official installation guide](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)
19 |
20 | ## Installation for Local Development
21 |
22 | Run `make`. This will build a `terraform-provider-ansible` binary in the top level of the project. To get Terraform to use this binary, configure the [development overrides](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers) for the provider installation. The easiest way to do this will be to create a config file with the following contents:
23 |
24 | ```
25 | provider_installation {
26 | dev_overrides {
27 | "ansible/ansible" = "/path/to/project/root"
28 | }
29 |
30 | direct {}
31 | }
32 | ```
33 |
34 | The `/path/to/project/root` should point to the location where you have cloned this repo, where the `terraform-provider-ansible` binary will be built. You can then set the `TF_CLI_CONFIG_FILE` environment variable to point to this config file, and Terraform will use the provider binary you just built.
35 |
36 | ### Testing
37 |
38 | Lint:
39 |
40 | ```shell
41 | curl -L https://github.com/golangci/golangci-lint/releases/download/v1.50.1/golangci-lint-1.50.1-linux-amd64.tar.gz \
42 | | tar --wildcards -xzf - --strip-components 1 "**/golangci-lint"
43 | curl -L https://github.com/nektos/act/releases/download/v0.2.34/act_Linux_x86_64.tar.gz \
44 | | tar -xzf - act
45 |
46 | # linters
47 | ./golangci-lint run -v
48 |
49 | # tests
50 | make test
51 |
52 | # GH actions locally
53 | ./act
54 | ```
55 |
56 | ### Examples
57 | The [examples](./examples/) subdirectory contains a usage example for this provider.
58 |
59 | ## Release notes
60 |
61 | See the [generated changelog](https://github.com/ansible/terraform-provider-ansible/tree/main/CHANGELOG.rst).
62 |
63 | ## Releasing
64 |
65 | To release a new version of the provider:
66 |
67 | 1. Update the version number in https://github.com/ansible/terraform-provider-ansible/blob/main/examples/provider/provider.tf
68 | 2. Run `go generate` to regenerate docs
69 | 3. Run `antsibull-changelog release --version ` to release a new version of the project.
70 | 4. Commit changes
71 | 5. Push a new tag (this should trigger an automated release process to the Terraform Registry)
72 | 6. Verify the new version is published at https://registry.terraform.io/providers/ansible/ansible/latest
73 |
74 | ## Licensing
75 |
76 | GNU General Public License v3.0. See [LICENSE](/LICENSE) for full text.
77 |
--------------------------------------------------------------------------------
/tests/expected_tfstate.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 4,
3 | "terraform_version": "1.11.4",
4 | "serial": 4,
5 | "lineage": "b73c1dcc-9c4a-3202-27dc-9240df9287e4",
6 | "outputs": {},
7 | "resources": [
8 | {
9 | "mode": "managed",
10 | "type": "ansible_group",
11 | "name": "group",
12 | "provider": "provider[\"registry.terraform.io/ansible/ansible\"]",
13 | "instances": [
14 | {
15 | "schema_version": 0,
16 | "attributes": {
17 | "children": [
18 | "somechild"
19 | ],
20 | "id": "somegroup",
21 | "name": "somegroup",
22 | "variables": {
23 | "hello": "from group!"
24 | }
25 | },
26 | "sensitive_attributes": [],
27 | "identity_schema_version": 0,
28 | "private": "bnVsbA=="
29 | }
30 | ]
31 | },
32 | {
33 | "mode": "managed",
34 | "type": "ansible_host",
35 | "name": "host",
36 | "provider": "provider[\"registry.terraform.io/ansible/ansible\"]",
37 | "instances": [
38 | {
39 | "schema_version": 0,
40 | "attributes": {
41 | "groups": [
42 | "somegroup"
43 | ],
44 | "id": "somehost",
45 | "name": "somehost",
46 | "variables": {
47 | "greetings": "from host!",
48 | "some": "variable",
49 | "yaml_hello": "from vault!",
50 | "yaml_list": "[\"some\",\"nice\",\"list\"]",
51 | "yaml_number": "24356"
52 | }
53 | },
54 | "sensitive_attributes": [
55 | [
56 | {
57 | "type": "get_attr",
58 | "value": "variables"
59 | },
60 | {
61 | "type": "index",
62 | "value": {
63 | "value": "yaml_hello",
64 | "type": "string"
65 | }
66 | }
67 | ],
68 | [
69 | {
70 | "type": "get_attr",
71 | "value": "variables"
72 | },
73 | {
74 | "type": "index",
75 | "value": {
76 | "value": "yaml_list",
77 | "type": "string"
78 | }
79 | }
80 | ],
81 | [
82 | {
83 | "type": "get_attr",
84 | "value": "variables"
85 | },
86 | {
87 | "type": "index",
88 | "value": {
89 | "value": "yaml_number",
90 | "type": "string"
91 | }
92 | }
93 | ]
94 | ],
95 | "identity_schema_version": 0,
96 | "private": "bnVsbA==",
97 | "dependencies": [
98 | "ansible_vault.secrets"
99 | ]
100 | }
101 | ]
102 | },
103 | {
104 | "mode": "managed",
105 | "type": "ansible_vault",
106 | "name": "secrets",
107 | "provider": "provider[\"registry.terraform.io/ansible/ansible\"]",
108 | "instances": [
109 | {
110 | "schema_version": 0,
111 | "attributes": {
112 | "args": [
113 | "view",
114 | "--vault-id",
115 | "testvault@vault_password",
116 | "vault-encrypted.yml"
117 | ],
118 | "id": "vault-encrypted.yml",
119 | "vault_file": "vault-encrypted.yml",
120 | "vault_id": "testvault",
121 | "vault_password_file": "vault_password",
122 | "yaml": "hello: from vault!\na_number: 24356\na_list:\n - some\n - nice\n - list\n"
123 | },
124 | "sensitive_attributes": [
125 | [
126 | {
127 | "type": "get_attr",
128 | "value": "yaml"
129 | }
130 | ]
131 | ],
132 | "identity_schema_version": 0,
133 | "private": "bnVsbA=="
134 | }
135 | ]
136 | }
137 | ],
138 | "check_results": null
139 | }
140 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ansible/terraform-provider-ansible
2 |
3 | go 1.24.4
4 |
5 | require (
6 | github.com/Jeffail/gabs v1.4.0
7 | github.com/hashicorp/terraform-plugin-docs v0.24.0
8 | github.com/hashicorp/terraform-plugin-framework v1.16.1
9 | github.com/hashicorp/terraform-plugin-go v0.29.0
10 | github.com/hashicorp/terraform-plugin-log v0.9.0
11 | github.com/hashicorp/terraform-plugin-mux v0.21.0
12 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1
13 | github.com/stretchr/testify v1.10.0
14 | gopkg.in/ini.v1 v1.67.0
15 | )
16 |
17 | require (
18 | github.com/BurntSushi/toml v1.2.1 // indirect
19 | github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
20 | github.com/Masterminds/goutils v1.1.1 // indirect
21 | github.com/Masterminds/semver/v3 v3.2.1 // indirect
22 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect
23 | github.com/ProtonMail/go-crypto v1.1.6 // indirect
24 | github.com/agext/levenshtein v1.2.3 // indirect
25 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
26 | github.com/armon/go-radix v1.0.0 // indirect
27 | github.com/bgentry/speakeasy v0.1.0 // indirect
28 | github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
29 | github.com/cloudflare/circl v1.6.1 // indirect
30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
31 | github.com/fatih/color v1.16.0 // indirect
32 | github.com/golang/protobuf v1.5.4 // indirect
33 | github.com/google/go-cmp v0.7.0 // indirect
34 | github.com/google/uuid v1.6.0 // indirect
35 | github.com/hashicorp/cli v1.1.7 // indirect
36 | github.com/hashicorp/errwrap v1.1.0 // indirect
37 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect
38 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
39 | github.com/hashicorp/go-cty v1.5.0 // indirect
40 | github.com/hashicorp/go-hclog v1.6.3 // indirect
41 | github.com/hashicorp/go-multierror v1.1.1 // indirect
42 | github.com/hashicorp/go-plugin v1.7.0 // indirect
43 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
44 | github.com/hashicorp/go-uuid v1.0.3 // indirect
45 | github.com/hashicorp/go-version v1.7.0 // indirect
46 | github.com/hashicorp/hc-install v0.9.2 // indirect
47 | github.com/hashicorp/hcl/v2 v2.24.0 // indirect
48 | github.com/hashicorp/logutils v1.0.0 // indirect
49 | github.com/hashicorp/terraform-exec v0.24.0 // indirect
50 | github.com/hashicorp/terraform-json v0.27.2 // indirect
51 | github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
52 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect
53 | github.com/hashicorp/yamux v0.1.2 // indirect
54 | github.com/huandu/xstrings v1.4.0 // indirect
55 | github.com/imdario/mergo v0.3.15 // indirect
56 | github.com/mattn/go-colorable v0.1.14 // indirect
57 | github.com/mattn/go-isatty v0.0.20 // indirect
58 | github.com/mattn/go-runewidth v0.0.9 // indirect
59 | github.com/mitchellh/copystructure v1.2.0 // indirect
60 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect
61 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect
62 | github.com/mitchellh/mapstructure v1.5.0 // indirect
63 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
64 | github.com/oklog/run v1.1.0 // indirect
65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
66 | github.com/posener/complete v1.2.3 // indirect
67 | github.com/shopspring/decimal v1.3.1 // indirect
68 | github.com/spf13/cast v1.5.0 // indirect
69 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
70 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
71 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
72 | github.com/yuin/goldmark v1.7.7 // indirect
73 | github.com/yuin/goldmark-meta v1.1.0 // indirect
74 | github.com/zclconf/go-cty v1.17.0 // indirect
75 | go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
76 | golang.org/x/crypto v0.42.0 // indirect
77 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
78 | golang.org/x/mod v0.28.0 // indirect
79 | golang.org/x/net v0.44.0 // indirect
80 | golang.org/x/sync v0.17.0 // indirect
81 | golang.org/x/sys v0.36.0 // indirect
82 | golang.org/x/text v0.30.0 // indirect
83 | golang.org/x/tools v0.37.0 // indirect
84 | google.golang.org/appengine v1.6.8 // indirect
85 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
86 | google.golang.org/grpc v1.75.1 // indirect
87 | google.golang.org/protobuf v1.36.9 // indirect
88 | gopkg.in/yaml.v2 v2.3.0 // indirect
89 | gopkg.in/yaml.v3 v3.0.1 // indirect
90 | )
91 |
--------------------------------------------------------------------------------
/providerutils/utils.go:
--------------------------------------------------------------------------------
1 | package providerutils
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path/filepath"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12 | "gopkg.in/ini.v1"
13 | )
14 |
15 | /*
16 | CREATE OPTIONS
17 | */
18 |
19 | const DefaultHostGroup = "default"
20 |
21 | func InterfaceToString(arr []interface{}) ([]string, diag.Diagnostics) {
22 | var diags diag.Diagnostics
23 |
24 | result := []string{}
25 |
26 | for _, val := range arr {
27 | tmpVal, ok := val.(string)
28 | if !ok {
29 | diags = append(diags, diag.Diagnostic{
30 | Severity: diag.Error,
31 | Summary: "Error: couldn't parse value to string!",
32 | })
33 | }
34 |
35 | result = append(result, tmpVal)
36 | }
37 |
38 | return result, diags
39 | }
40 |
41 | // Create a "verbpse" switch
42 | // example: verbosity = 2 --> verbose_switch = "-vv"
43 | func CreateVerboseSwitch(verbosity int) string {
44 | verbose := ""
45 |
46 | if verbosity == 0 {
47 | return verbose
48 | }
49 |
50 | verbose += "-"
51 | verbose += strings.Repeat("v", verbosity)
52 |
53 | return verbose
54 | }
55 |
56 | // Build inventory.ini (NOT YAML)
57 | // -- building inventory.ini is easier
58 |
59 | func BuildPlaybookInventory(
60 | inventoryDest string,
61 | hostname string,
62 | port int,
63 | hostgroups []interface{},
64 | ) (string, diag.Diagnostics) {
65 | var diags diag.Diagnostics
66 | // Check if inventory file is already present
67 | // if not, create one
68 | fileInfo, err := os.CreateTemp("", inventoryDest)
69 | if err != nil {
70 | diags = append(diags, diag.Diagnostic{
71 | Severity: diag.Error,
72 | Summary: fmt.Sprintf("Fail to create inventory file: %v", err),
73 | })
74 | }
75 |
76 | tempFileName := fileInfo.Name()
77 | log.Printf("Inventory %s was created", fileInfo.Name())
78 |
79 | // Then, read inventory and add desired settings to it
80 | inventory, err := ini.Load(tempFileName)
81 | if err != nil {
82 | diags = append(diags, diag.Diagnostic{
83 | Severity: diag.Error,
84 | Summary: fmt.Sprintf("Fail to read inventory: %v", err),
85 | })
86 | }
87 |
88 | tempHostgroups := hostgroups
89 |
90 | if len(tempHostgroups) == 0 {
91 | tempHostgroups = append(tempHostgroups, DefaultHostGroup)
92 | }
93 |
94 | if len(tempHostgroups) > 0 { // if there is a list of groups specified for the desired host
95 | for _, hostgroup := range tempHostgroups {
96 | hostgroupStr, okay := hostgroup.(string)
97 | if !okay {
98 | diags = append(diags, diag.Diagnostic{
99 | Severity: diag.Error,
100 | Summary: "Couldn't assert type: string",
101 | })
102 | }
103 |
104 | if !inventory.HasSection(hostgroupStr) {
105 | _, err := inventory.NewRawSection(hostgroupStr, "")
106 | if err != nil {
107 | diags = append(diags, diag.Diagnostic{
108 | Severity: diag.Error,
109 | Summary: fmt.Sprintf("Fail to create a hostgroup: %v", err),
110 | })
111 | }
112 | }
113 |
114 | if !inventory.Section(hostgroupStr).HasKey(hostname) {
115 | body := hostname
116 | if port != -1 {
117 | body += " ansible_port=" + strconv.Itoa(port)
118 | }
119 |
120 | inventory.Section(hostgroupStr).SetBody(body)
121 | }
122 | }
123 | }
124 |
125 | err = inventory.SaveTo(tempFileName)
126 | if err != nil {
127 | diags = append(diags, diag.Diagnostic{
128 | Severity: diag.Error,
129 | Summary: fmt.Sprintf("Fail to create inventory: %v", err),
130 | })
131 | }
132 |
133 | return tempFileName, diags
134 | }
135 |
136 | func RemoveFile(filename string) diag.Diagnostics {
137 | var diags diag.Diagnostics
138 |
139 | err := os.Remove(filename)
140 | if err != nil {
141 | diags = append(diags, diag.Diagnostic{
142 | Severity: diag.Error,
143 | Summary: fmt.Sprintf("Fail to remove file %s: %v", filename, err),
144 | })
145 | }
146 |
147 | return diags
148 | }
149 |
150 | func GetAllInventories(inventoryPrefix string) ([]string, diag.Diagnostics) {
151 | var diags diag.Diagnostics
152 |
153 | tempDir := os.TempDir()
154 |
155 | log.Printf("[TEMP DIR]: %s", tempDir)
156 |
157 | files, err := os.ReadDir(tempDir)
158 | if err != nil {
159 | diags = append(diags, diag.Diagnostic{
160 | Severity: diag.Error,
161 | Summary: fmt.Sprintf("Fail to read dir %s: %v", tempDir, err),
162 | })
163 | }
164 |
165 | inventories := []string{}
166 |
167 | for _, file := range files {
168 | if strings.HasPrefix(file.Name(), inventoryPrefix) {
169 | inventoryAbsPath := filepath.Join(tempDir, file.Name())
170 | inventories = append(inventories, inventoryAbsPath)
171 | }
172 | }
173 |
174 | return inventories, diags
175 | }
176 |
--------------------------------------------------------------------------------
/provider/resource_vault.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "os/exec"
8 |
9 | "github.com/ansible/terraform-provider-ansible/providerutils"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12 | )
13 |
14 | func resourceVault() *schema.Resource {
15 | return &schema.Resource{
16 | CreateContext: resourceVaultCreate,
17 | ReadContext: resourceVaultRead,
18 | UpdateContext: resourceVaultUpdate,
19 | DeleteContext: resourceVaultDelete,
20 |
21 | Schema: map[string]*schema.Schema{
22 | "vault_file": {
23 | Type: schema.TypeString,
24 | Required: true,
25 | Optional: false,
26 | Description: "Path to encrypted vault file.",
27 | },
28 | "vault_password_file": {
29 | Type: schema.TypeString,
30 | Required: true,
31 | Optional: false,
32 | Description: "Path to vault password file.",
33 | },
34 |
35 | "vault_id": {
36 | Type: schema.TypeString,
37 | Required: false,
38 | Optional: true,
39 | Default: "",
40 | Description: "ID of the encrypted vault file.",
41 | },
42 |
43 | // computed
44 | "yaml": {
45 | Type: schema.TypeString,
46 | Computed: true,
47 | Sensitive: true,
48 | },
49 |
50 | // computed - for debug
51 | "args": {
52 | Type: schema.TypeList,
53 | Computed: true,
54 | Elem: &schema.Schema{Type: schema.TypeString},
55 | },
56 | },
57 | }
58 | }
59 |
60 | func resourceVaultCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
61 | var diags diag.Diagnostics
62 |
63 | vaultFile, okay := data.Get("vault_file").(string)
64 |
65 | if !okay {
66 | diags = append(diags, diag.Diagnostic{
67 | Severity: diag.Error,
68 | Summary: "WARNING [ansible-vault]: couldn't get 'vault_file'!",
69 | })
70 | }
71 |
72 | vaultPasswordFile, okay := data.Get("vault_password_file").(string)
73 | if !okay {
74 | diags = append(diags, diag.Diagnostic{
75 | Severity: diag.Error,
76 | Summary: "WARNING [ansible-vault]: couldn't get 'vault_password_file'!",
77 | })
78 | }
79 |
80 | vaultID, okay := data.Get("vault_id").(string)
81 | if !okay {
82 | diags = append(diags, diag.Diagnostic{
83 | Severity: diag.Error,
84 | Summary: "WARNING [ansible-vault]: couldn't get 'vault_id'!",
85 | })
86 | }
87 |
88 | data.SetId(vaultFile)
89 |
90 | var args interface{}
91 |
92 | // Compute arguments (args)
93 | if vaultID != "" {
94 | args = []string{
95 | "view",
96 | "--vault-id",
97 | vaultID + "@" + vaultPasswordFile,
98 | vaultFile,
99 | }
100 | } else {
101 | args = []string{
102 | "view",
103 | "--vault-password-file",
104 | vaultPasswordFile,
105 | vaultFile,
106 | }
107 | }
108 |
109 | log.Print("LOG [ansible-vault]: ARGS")
110 | log.Print(args)
111 |
112 | if err := data.Set("args", args); err != nil {
113 | diags = append(diags, diag.Diagnostic{
114 | Severity: diag.Error,
115 | Summary: fmt.Sprintf("ERROR [ansible-vault]: couldn't calculate 'args' variable! %s", err),
116 | Detail: ansiblePlaybook,
117 | })
118 | }
119 |
120 | diagsFromRead := resourceVaultRead(ctx, data, meta)
121 | diags = append(diags, diagsFromRead...)
122 |
123 | return diags
124 | }
125 |
126 | func resourceVaultRead(_ context.Context, data *schema.ResourceData, _ interface{}) diag.Diagnostics {
127 | var diags diag.Diagnostics
128 |
129 | vaultFile, okay := data.Get("vault_file").(string)
130 |
131 | if !okay {
132 | diags = append(diags, diag.Diagnostic{
133 | Severity: diag.Warning,
134 | Summary: "WARNING [ansible-vault]: couldn't get 'vault_file'!",
135 | })
136 | }
137 |
138 | vaultPasswordFile, okay := data.Get("vault_password_file").(string)
139 | if !okay {
140 | diags = append(diags, diag.Diagnostic{
141 | Severity: diag.Warning,
142 | Summary: "WARNING [ansible-vault]: couldn't get 'vault_password_file'!",
143 | })
144 | }
145 |
146 | argsTerraform, okay := data.Get("args").([]interface{})
147 | if !okay {
148 | diags = append(diags, diag.Diagnostic{
149 | Severity: diag.Warning,
150 | Summary: "WARNING [ansible-vault]: couldn't get 'args'!",
151 | })
152 | }
153 |
154 | log.Printf("LOG [ansible-vault]: vault_file = %s, vault_password_file = %s\n", vaultFile, vaultPasswordFile)
155 |
156 | args, diagsFromUtils := providerutils.InterfaceToString(argsTerraform)
157 |
158 | diags = append(diags, diagsFromUtils...)
159 |
160 | cmd := exec.Command("ansible-vault", args...)
161 |
162 | yamlString, err := cmd.CombinedOutput()
163 | if err != nil {
164 | diags = append(diags, diag.Diagnostic{
165 | Severity: diag.Error,
166 | Summary: string(yamlString),
167 | Detail: ansiblePlaybook,
168 | })
169 | }
170 |
171 | if err := data.Set("yaml", string(yamlString)); err != nil {
172 | diags = append(diags, diag.Diagnostic{
173 | Severity: diag.Error,
174 | Summary: fmt.Sprintf("ERROR [ansible-vault]: couldn't calculate 'yaml' variable! %s", err),
175 | Detail: ansiblePlaybook,
176 | })
177 | }
178 |
179 | return diags
180 | }
181 |
182 | func resourceVaultUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
183 | return resourceVaultRead(ctx, data, meta)
184 | }
185 |
186 | func resourceVaultDelete(_ context.Context, data *schema.ResourceData, _ interface{}) diag.Diagnostics {
187 | data.SetId("")
188 |
189 | return nil
190 | }
191 |
--------------------------------------------------------------------------------
/examples/ansible_playbook/end-to-end.tf:
--------------------------------------------------------------------------------
1 | resource "docker_container" "alpine_1" {
2 | image = docker_image.julia.image_id
3 | name = "julia"
4 | must_run = true
5 |
6 | command = [
7 | "sleep",
8 | "infinity"
9 | ]
10 | }
11 |
12 |
13 |
14 | # Test resources:
15 | # - e2e-vars
16 | # - e2e-vault
17 | # - e2e-groups
18 | # - e2e-limit-positive
19 | # - e2e-limit-negative
20 | # - e2e-tags
21 |
22 |
23 | # NOTE: [ SUCCESS ]
24 | resource "ansible_playbook" "e2e_vars" {
25 | ansible_playbook_binary = "ansible-playbook"
26 | playbook = "end-to-end-playbook.yml"
27 |
28 | # inventory configuration
29 | name = docker_container.alpine_1.name
30 |
31 | # play control
32 | var_files = [
33 | "var-file.yml"
34 | ]
35 |
36 | # connection configuration and other vars
37 | extra_vars = {
38 | ansible_hostname = docker_container.alpine_1.name
39 | ansible_connection = "docker"
40 | injected_variable = "content of an injected variable"
41 |
42 | test_filename = "test_e2e_vars.txt"
43 | }
44 | }
45 |
46 |
47 | # NOTE: [ SUCCESS ]
48 | resource "ansible_playbook" "e2e_vault" {
49 | ansible_playbook_binary = "ansible-playbook"
50 | playbook = "end-to-end-playbook.yml"
51 |
52 | # inventory configuration
53 | name = docker_container.alpine_1.name
54 |
55 | # ansible vault
56 | vault_password_file = "vault-password-file.txt"
57 | vault_files = [
58 | "vault-file.yml",
59 | ]
60 |
61 | # connection configuration and other vars
62 | extra_vars = {
63 | ansible_hostname = docker_container.alpine_1.name
64 | ansible_connection = "docker"
65 |
66 | test_filename = "test_e2e_vault.txt"
67 | }
68 |
69 | depends_on = [ansible_playbook.e2e_vars] # make sure this resource waits for e2e_vars to finish
70 | }
71 |
72 |
73 | # NOTE: [ SUCCESS ]
74 | resource "ansible_playbook" "e2e_groups" {
75 | ansible_playbook_binary = "ansible-playbook"
76 | playbook = "end-to-end-playbook.yml"
77 |
78 | # inventory configuration
79 | name = docker_container.alpine_1.name
80 | groups = ["this_group_exists"]
81 |
82 | # connection configuration and other vars
83 | extra_vars = {
84 | ansible_hostname = docker_container.alpine_1.name
85 | ansible_connection = "docker"
86 |
87 | test_filename = "test_e2e_groups.txt"
88 | }
89 |
90 | depends_on = [ansible_playbook.e2e_vault] # make sure this resource waits for e2e_vault to finish
91 | }
92 |
93 |
94 | # NOTE: [ SUCCESS ]
95 | resource "ansible_playbook" "e2e_limit_positive" {
96 | ansible_playbook_binary = "ansible-playbook"
97 | playbook = "end-to-end-playbook.yml"
98 |
99 | # inventory configuration
100 | name = docker_container.alpine_1.name
101 |
102 | limit = [
103 | docker_container.alpine_1.name
104 | ]
105 |
106 | # connection configuration and other vars
107 | extra_vars = {
108 | ansible_hostname = docker_container.alpine_1.name
109 | ansible_connection = "docker"
110 |
111 | test_filename = "test_e2e_limit_positive.txt"
112 | }
113 |
114 | depends_on = [ansible_playbook.e2e_groups] # make sure this resource waits for e2e_groups to finish
115 | }
116 |
117 |
118 | # NOTE: [ FAIL ]
119 | # -- this resource is supposed to fail,
120 | # so the playbook failure is being ignored
121 | resource "ansible_playbook" "e2e_limit_negative" {
122 | ignore_playbook_failure = true # set to 'true' because it's supposed to fail
123 |
124 | ansible_playbook_binary = "ansible-playbook"
125 | playbook = "end-to-end-playbook.yml"
126 |
127 | # inventory configuration
128 | name = docker_container.alpine_1.name
129 | check_mode = true
130 |
131 | limit = [
132 | "nonexistent_host"
133 | ]
134 |
135 | # connection configuration and other vars
136 | extra_vars = {
137 | ansible_hostname = docker_container.alpine_1.name
138 | ansible_connection = "docker"
139 |
140 | test_filename = "test_e2e_limit_negative.txt"
141 | }
142 |
143 | verbosity = 3
144 |
145 | depends_on = [ansible_playbook.e2e_limit_positive] # make sure this resource waits for e2e_limit_positive to finish
146 | }
147 |
148 |
149 | # NOTE: [ SUCCESS ]
150 | resource "ansible_playbook" "e2e_tags" {
151 | ansible_playbook_binary = "ansible-playbook"
152 | playbook = "end-to-end-playbook.yml"
153 |
154 | # inventory configuration
155 | name = docker_container.alpine_1.name
156 |
157 | tags = [
158 | "tag1",
159 | "tag2"
160 | ]
161 |
162 | # connection configuration and other vars
163 | extra_vars = {
164 | ansible_hostname = docker_container.alpine_1.name
165 | ansible_connection = "docker"
166 |
167 | test_filename = "test_e2e_tags.txt"
168 | }
169 |
170 | depends_on = [ansible_playbook.e2e_limit_negative] # make sure this resource waits for e2e_limit_negative to finish
171 | }
172 |
173 |
174 | # NOTE: [ SUCCESS ]
175 | resource "ansible_playbook" "e2e_tags_1" {
176 | ansible_playbook_binary = "ansible-playbook"
177 | playbook = "end-to-end-playbook.yml"
178 |
179 | # inventory configuration
180 | name = docker_container.alpine_1.name
181 |
182 | tags = [
183 | "tag1"
184 | ]
185 |
186 | # connection configuration and other vars
187 | extra_vars = {
188 | ansible_hostname = docker_container.alpine_1.name
189 | ansible_connection = "docker"
190 |
191 | test_filename = "test_e2e_tags_1.txt"
192 | }
193 |
194 | depends_on = [ansible_playbook.e2e_tags] # make sure this resource waits for e2e_tags to finish
195 | }
196 |
197 |
198 | # NOTE: [ SUCCESS ]
199 | resource "ansible_playbook" "e2e_tags_2" {
200 | ansible_playbook_binary = "ansible-playbook"
201 | playbook = "end-to-end-playbook.yml"
202 |
203 | # inventory configuration
204 | name = docker_container.alpine_1.name
205 |
206 | tags = [
207 | "tag2"
208 | ]
209 |
210 | # connection configuration and other vars
211 | extra_vars = {
212 | ansible_hostname = docker_container.alpine_1.name
213 | ansible_connection = "docker"
214 |
215 | test_filename = "test_e2e_tags_2.txt"
216 | }
217 |
218 | depends_on = [ansible_playbook.e2e_tags_1] # make sure this resource waits for e2e_tags_1 to finish
219 | }
220 |
--------------------------------------------------------------------------------
/docs/data-sources/inventory.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "ansible_inventory DataSource - terraform-provider-ansible"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # ansible_inventory (DataSource)
10 |
11 | This data source represents an ansible inventory. It has a json attribute containing the JSON representation of the inventory.
12 |
13 | ## Example Usage
14 | ```terraform
15 | data "ansible_inventory" "myinventory" {
16 | group {
17 | name = "webservers"
18 |
19 | host {
20 | name = aws_instance.web.public_ip
21 | ansible_user = "ubuntu"
22 | ansible_private_key_file = local_file.private_key.filename
23 | ansible_ssh_extra_args = "-o StrictHostKeyChecking=no"
24 | }
25 | }
26 |
27 | group {
28 | name = "dbservers"
29 |
30 | host {
31 | name = aws_instance.primary_db.public_ip
32 | ansible_user = "root"
33 | ansible_private_key_file = local_file.private_key.filename
34 | }
35 |
36 | host {
37 | name = aws_instance.fallback_db.public_ip
38 | ansible_user = "root"
39 | ansible_private_key_file = local_file.private_key.filename
40 | }
41 | }
42 | }
43 |
44 | # If you need the inventory as a file you can use the local_file resource
45 | resource "local_file" "myinventory" {
46 | content = ansible_inventory.myinventory.json
47 | filename = "${path.module}/inventory.json"
48 | }
49 |
50 | # It can also be used directly in an Action
51 | action "ansible_playbook_run" "ansible" {
52 | config {
53 | playbooks = ["${path.module}/playbook.yml"]
54 | inventories = [data.ansible_inventory.host.json]
55 | }
56 | }
57 | ```
58 |
59 |
60 | ## Schema
61 |
62 | ### Optional
63 |
64 | - `group` (Block List) Describes an ansible group. (see [below for nested schema](#nestedblock--group))
65 |
66 | ### Read-Only
67 |
68 | - `json` (String, Sensitive) The JSON content of the inventory file.
69 |
70 |
71 | ### Nested Schema for `group`
72 |
73 | Required:
74 |
75 | - `name` (String) Name of the group.
76 |
77 | Optional:
78 |
79 | - `group` (Block List) Describes an ansible group. (see [below for nested schema](#nestedblock--group--group))
80 | - `host` (Block List) Describes an ansible host. (see [below for nested schema](#nestedblock--group--host))
81 | - `vars` (Map of String) Variables to be set for the group.
82 |
83 |
84 | ### Nested Schema for `group.group`
85 |
86 | Required:
87 |
88 | - `name` (String) Name of the group.
89 |
90 | Optional:
91 |
92 | - `group` (Block List) Describes an ansible group. (see [below for nested schema](#nestedblock--group--group--group))
93 | - `host` (Block List) Describes an ansible host. (see [below for nested schema](#nestedblock--group--group--host))
94 | - `vars` (Map of String) Variables to be set for the group.
95 |
96 |
97 | ### Nested Schema for `group.group.group`
98 |
99 | Required:
100 |
101 | - `name` (String) Name of the group.
102 |
103 | Optional:
104 |
105 | - `host` (Block List) Describes an ansible host. (see [below for nested schema](#nestedblock--group--group--group--host))
106 | - `vars` (Map of String) Variables to be set for the group.
107 |
108 |
109 | ### Nested Schema for `group.group.group.host`
110 |
111 | Required:
112 |
113 | - `name` (String) Name of the host.
114 |
115 | Optional:
116 |
117 | - `ansible_become` (Boolean) Allows you to force privilege escalation.
118 | - `ansible_become_exe` (String) Allows you to set the executable for the escalation method you selected.
119 | - `ansible_become_flags` (String) Allows you to set the flags passed to the selected escalation method
120 | - `ansible_become_method` (String) Allows you to set the privilege escalation method to a matching become plugin.
121 | - `ansible_become_password` (String, Sensitive) Allows you to set the privilege escalation password.
122 | - `ansible_become_user` (String) Allows you to set the user you become through privilege escalation.
123 | - `ansible_connection` (String) Specifies the connection type to the host. This can be the name of any Ansible connection plugin. SSH protocol types are ssh or paramiko. The default is ssh.
124 | - `ansible_host` (String) Specifies the resolvable name or IP of the host to connect to, if it is different from the alias (name) you wish to give to it.
125 | - `ansible_password` (String, Sensitive) The password to use when connecting (logging in) to the host.
126 | - `ansible_port` (Number) The connection port number, if not the default (22 for ssh).
127 | - `ansible_private_key_file` (String) Private key file used by SSH. This is useful if you use multiple keys and you do not want to use SSH agent.
128 | - `ansible_python_interpreter` (String) Allows you to set the Python interpreter to use for the target system.
129 | - `ansible_scp_extra_args` (String) Extra arguments to pass to the scp command.
130 | - `ansible_sftp_extra_args` (String) Extra arguments to pass to the sftp command.
131 | - `ansible_shell_executable` (String) Allows you to set the shell executable to use for the target system.
132 | - `ansible_shell_type` (String) Specifies the shell type of the target system. You should not use this setting unless you have set the `ansible_shell_executable` to a non-Bourne (sh) compatible shell. By default, Ansible formats commands using sh-style syntax. If you set this to csh or fish, commands that Ansible executes on target systems follow those shell’s syntax instead.
133 | - `ansible_ssh_common_args` (String) Ansible always appends this setting to the default command line for sftp, scp, and ssh. This is useful for configuring a ``ProxyCommand` for a certain host or group.
134 | - `ansible_ssh_executable` (String) Specify the SSH executable to use.
135 | - `ansible_ssh_extra_args` (String) Extra arguments to pass to the ssh command.
136 | - `ansible_ssh_pipelining` (Boolean) Enable pipelining for SSH connections.
137 | - `ansible_user` (String) The username to use when connecting (logging in) to the host.
138 |
139 |
140 |
141 |
142 | ### Nested Schema for `group.group.host`
143 |
144 | Required:
145 |
146 | - `name` (String) Name of the host.
147 |
148 | Optional:
149 |
150 | - `ansible_become` (Boolean) Allows you to force privilege escalation.
151 | - `ansible_become_exe` (String) Allows you to set the executable for the escalation method you selected.
152 | - `ansible_become_flags` (String) Allows you to set the flags passed to the selected escalation method
153 | - `ansible_become_method` (String) Allows you to set the privilege escalation method to a matching become plugin.
154 | - `ansible_become_password` (String, Sensitive) Allows you to set the privilege escalation password.
155 | - `ansible_become_user` (String) Allows you to set the user you become through privilege escalation.
156 | - `ansible_connection` (String) Specifies the connection type to the host. This can be the name of any Ansible connection plugin. SSH protocol types are ssh or paramiko. The default is ssh.
157 | - `ansible_host` (String) Specifies the resolvable name or IP of the host to connect to, if it is different from the alias (name) you wish to give to it.
158 | - `ansible_password` (String, Sensitive) The password to use when connecting (logging in) to the host.
159 | - `ansible_port` (Number) The connection port number, if not the default (22 for ssh).
160 | - `ansible_private_key_file` (String) Private key file used by SSH. This is useful if you use multiple keys and you do not want to use SSH agent.
161 | - `ansible_python_interpreter` (String) Allows you to set the Python interpreter to use for the target system.
162 | - `ansible_scp_extra_args` (String) Extra arguments to pass to the scp command.
163 | - `ansible_sftp_extra_args` (String) Extra arguments to pass to the sftp command.
164 | - `ansible_shell_executable` (String) Allows you to set the shell executable to use for the target system.
165 | - `ansible_shell_type` (String) Specifies the shell type of the target system. You should not use this setting unless you have set the `ansible_shell_executable` to a non-Bourne (sh) compatible shell. By default, Ansible formats commands using sh-style syntax. If you set this to csh or fish, commands that Ansible executes on target systems follow those shell’s syntax instead.
166 | - `ansible_ssh_common_args` (String) Ansible always appends this setting to the default command line for sftp, scp, and ssh. This is useful for configuring a ``ProxyCommand` for a certain host or group.
167 | - `ansible_ssh_executable` (String) Specify the SSH executable to use.
168 | - `ansible_ssh_extra_args` (String) Extra arguments to pass to the ssh command.
169 | - `ansible_ssh_pipelining` (Boolean) Enable pipelining for SSH connections.
170 | - `ansible_user` (String) The username to use when connecting (logging in) to the host.
171 |
172 |
173 |
174 |
175 | ### Nested Schema for `group.host`
176 |
177 | Required:
178 |
179 | - `name` (String) Name of the host.
180 |
181 | Optional:
182 |
183 | - `ansible_become` (Boolean) Allows you to force privilege escalation.
184 | - `ansible_become_exe` (String) Allows you to set the executable for the escalation method you selected.
185 | - `ansible_become_flags` (String) Allows you to set the flags passed to the selected escalation method
186 | - `ansible_become_method` (String) Allows you to set the privilege escalation method to a matching become plugin.
187 | - `ansible_become_password` (String, Sensitive) Allows you to set the privilege escalation password.
188 | - `ansible_become_user` (String) Allows you to set the user you become through privilege escalation.
189 | - `ansible_connection` (String) Specifies the connection type to the host. This can be the name of any Ansible connection plugin. SSH protocol types are ssh or paramiko. The default is ssh.
190 | - `ansible_host` (String) Specifies the resolvable name or IP of the host to connect to, if it is different from the alias (name) you wish to give to it.
191 | - `ansible_password` (String, Sensitive) The password to use when connecting (logging in) to the host.
192 | - `ansible_port` (Number) The connection port number, if not the default (22 for ssh).
193 | - `ansible_private_key_file` (String) Private key file used by SSH. This is useful if you use multiple keys and you do not want to use SSH agent.
194 | - `ansible_python_interpreter` (String) Allows you to set the Python interpreter to use for the target system.
195 | - `ansible_scp_extra_args` (String) Extra arguments to pass to the scp command.
196 | - `ansible_sftp_extra_args` (String) Extra arguments to pass to the sftp command.
197 | - `ansible_shell_executable` (String) Allows you to set the shell executable to use for the target system.
198 | - `ansible_shell_type` (String) Specifies the shell type of the target system. You should not use this setting unless you have set the `ansible_shell_executable` to a non-Bourne (sh) compatible shell. By default, Ansible formats commands using sh-style syntax. If you set this to csh or fish, commands that Ansible executes on target systems follow those shell’s syntax instead.
199 | - `ansible_ssh_common_args` (String) Ansible always appends this setting to the default command line for sftp, scp, and ssh. This is useful for configuring a ``ProxyCommand` for a certain host or group.
200 | - `ansible_ssh_executable` (String) Specify the SSH executable to use.
201 | - `ansible_ssh_extra_args` (String) Extra arguments to pass to the ssh command.
202 | - `ansible_ssh_pipelining` (Boolean) Enable pipelining for SSH connections.
203 | - `ansible_user` (String) The username to use when connecting (logging in) to the host.
204 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/framework/data_inventory.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "maps"
7 |
8 | "github.com/hashicorp/terraform-plugin-framework/datasource"
9 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
10 | "github.com/hashicorp/terraform-plugin-framework/diag"
11 | "github.com/hashicorp/terraform-plugin-framework/types"
12 | )
13 |
14 | var (
15 | _ datasource.DataSource = (*InventoryDataSource)(nil)
16 | )
17 |
18 | type InventoryDataSource struct{}
19 |
20 | func NewInventoryDataSource() datasource.DataSource {
21 | return &InventoryDataSource{}
22 | }
23 |
24 | // Metadata implements datasource.Resource.
25 | func (i *InventoryDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
26 | resp.TypeName = req.ProviderTypeName + "_inventory"
27 | }
28 |
29 | type InventoryDataSourceModel struct {
30 | Groups types.List `tfsdk:"group"`
31 | Json types.String `tfsdk:"json"`
32 | }
33 |
34 | type SharedGroupModel struct {
35 | Name types.String `tfsdk:"name"`
36 | Vars types.Map `tfsdk:"vars"`
37 | Hosts types.List `tfsdk:"host"`
38 | }
39 |
40 | type NestedGroupModel struct {
41 | SharedGroupModel
42 | Groups types.List `tfsdk:"group"`
43 | }
44 |
45 | type FinalGroupModel struct {
46 | SharedGroupModel
47 | }
48 |
49 | // root plus two levels of nesting
50 | const groupNestingLevel = 2
51 |
52 | func inventoryToJson(irm *InventoryDataSourceModel) ([]byte, diag.Diagnostics) {
53 | jsonValue, diags := groupsToJson(irm.Groups, 0)
54 | if diags.HasError() {
55 | return nil, diags
56 | }
57 | ret, err := json.Marshal(jsonValue)
58 | if err != nil {
59 | diags.Append(diag.NewErrorDiagnostic("Could not marshal inventory to JSON", err.Error()))
60 | return nil, diags
61 | }
62 | return ret, nil
63 | }
64 |
65 | func groupsToJson(list types.List, level int) (map[string]json.RawMessage, diag.Diagnostics) {
66 | jsonValue := map[string]json.RawMessage{}
67 | var diags diag.Diagnostics
68 |
69 | if level < groupNestingLevel {
70 | // There is a deeper nesting level
71 | var nestedGroups []NestedGroupModel
72 | diags := list.ElementsAs(context.Background(), &nestedGroups, false)
73 | if diags.HasError() {
74 | return nil, diags
75 | }
76 | for _, group := range nestedGroups {
77 | groupJson, diags := nestedGroupToJson(group, level)
78 | if diags.HasError() {
79 | return nil, diags
80 | }
81 | b, err := json.Marshal(groupJson)
82 | if err != nil {
83 | diags.Append(diag.NewErrorDiagnostic("Could not marshal group to JSON", err.Error()))
84 | }
85 |
86 | jsonValue[group.Name.ValueString()] = b
87 | }
88 | } else {
89 | // This is the final nesting level
90 | var finalGroups []FinalGroupModel
91 | diags := list.ElementsAs(context.Background(), &finalGroups, false)
92 | if diags.HasError() {
93 | return nil, diags
94 | }
95 |
96 | for _, group := range finalGroups {
97 | groupJson, diags := sharedGroupToJson(group.SharedGroupModel)
98 | if diags.HasError() {
99 | return nil, diags
100 | }
101 | b, err := json.Marshal(groupJson)
102 | if err != nil {
103 | diags.Append(diag.NewErrorDiagnostic("Could not marshal group to JSON", err.Error()))
104 | }
105 |
106 | jsonValue[group.Name.ValueString()] = b
107 | }
108 | }
109 |
110 | return jsonValue, diags
111 | }
112 | func nestedGroupToJson(group NestedGroupModel, level int) (map[string]json.RawMessage, diag.Diagnostics) {
113 | jsonValue, diags := sharedGroupToJson(group.SharedGroupModel)
114 | if diags.HasError() {
115 | return nil, diags
116 | }
117 |
118 | groupsJson, diags := groupsToJson(group.Groups, level+1)
119 | if diags.HasError() {
120 | return nil, diags
121 | }
122 | b, err := json.Marshal(groupsJson)
123 | if err != nil {
124 | diags.Append(diag.NewErrorDiagnostic("Could not marshal nested groups to JSON", err.Error()))
125 | return nil, diags
126 | }
127 | jsonValue["children"] = b
128 |
129 | return jsonValue, diags
130 | }
131 |
132 | func sharedGroupToJson(group SharedGroupModel) (map[string]json.RawMessage, diag.Diagnostics) {
133 | jsonValue := map[string]json.RawMessage{}
134 |
135 | var hosts []HostModel
136 | diags := group.Hosts.ElementsAs(context.Background(), &hosts, false)
137 | if diags.HasError() {
138 | return nil, diags
139 | }
140 |
141 | hostsJson := map[string]json.RawMessage{}
142 | for _, host := range hosts {
143 | hostJson, diags := hostToJson(&host)
144 | if diags.HasError() {
145 | return nil, diags
146 | }
147 | hostsJson[host.Name.ValueString()] = hostJson
148 | }
149 | if len(hostsJson) > 0 {
150 | b, err := json.Marshal(hostsJson)
151 | if err != nil {
152 | diags.Append(diag.NewErrorDiagnostic("Could not marshal hosts to JSON", err.Error()))
153 | return nil, diags
154 | }
155 | jsonValue["hosts"] = b
156 | }
157 |
158 | var vars map[string]string
159 | diags = group.Vars.ElementsAs(context.Background(), &vars, false)
160 | if diags.HasError() {
161 | return nil, diags
162 | }
163 | if len(vars) > 0 {
164 | b, err := json.Marshal(vars)
165 | if err != nil {
166 | diags.Append(diag.NewErrorDiagnostic("Could not marshal vars to JSON", err.Error()))
167 | return nil, diags
168 | }
169 | jsonValue["vars"] = b
170 | }
171 |
172 | return jsonValue, diags
173 | }
174 |
175 | func hostToJson(hm *HostModel) (json.RawMessage, diag.Diagnostics) {
176 | diags := diag.Diagnostics{}
177 | ret, err := json.Marshal(JsonHostModel{
178 | AnsibleConnection: hm.AnsibleConnection.ValueString(),
179 | AnsibleHost: hm.AnsibleHost.ValueString(),
180 | AnsiblePort: hm.AnsiblePort.ValueInt64(),
181 | AnsibleUser: hm.AnsibleUser.ValueString(),
182 | AnsiblePassword: hm.AnsiblePassword.ValueString(),
183 | AnsiblePrivateKeyFile: hm.AnsiblePrivateKeyFile.ValueString(),
184 | AnsibleSSHCommonArgs: hm.AnsibleSSHCommonArgs.ValueString(),
185 | AnsibleSftpExtraArgs: hm.AnsibleSftpExtraArgs.ValueString(),
186 | AnsibleScpExtraArgs: hm.AnsibleScpExtraArgs.ValueString(),
187 | AnsibleSSHExtraArgs: hm.AnsibleSSHExtraArgs.ValueString(),
188 | AnsibleSSHPipelining: hm.AnsibleSSHPipelining.ValueBool(),
189 | AnsibleSSHExecutable: hm.AnsibleSSHExecutable.ValueString(),
190 | AnsibleBecome: hm.AnsibleBecome.ValueBool(),
191 | AnsibleBecomeMethod: hm.AnsibleBecomeMethod.ValueString(),
192 | AnsibleBecomeUser: hm.AnsibleBecomeUser.ValueString(),
193 | AnsibleBecomePassword: hm.AnsibleBecomePassword.ValueString(),
194 | AnsibleBecomeExe: hm.AnsibleBecomeExe.ValueString(),
195 | AnsibleBecomeFlags: hm.AnsibleBecomeFlags.ValueString(),
196 | AnsibleShellType: hm.AnsibleShellType.ValueString(),
197 | AnsiblePythonInterpreter: hm.AnsiblePythonInterpreter.ValueString(),
198 | AnsibleShellExecutable: hm.AnsibleShellExecutable.ValueString(),
199 | })
200 |
201 | if err != nil {
202 | diags.Append(diag.NewErrorDiagnostic("Could not marshal host when marshalling inventory to JSON", err.Error()))
203 | return nil, diags
204 | }
205 |
206 | return ret, diags
207 | }
208 |
209 | type HostModel struct {
210 | Name types.String `tfsdk:"name"`
211 | AnsibleConnection types.String `tfsdk:"ansible_connection"`
212 | AnsibleHost types.String `tfsdk:"ansible_host"`
213 | AnsiblePort types.Int64 `tfsdk:"ansible_port"`
214 | AnsibleUser types.String `tfsdk:"ansible_user"`
215 | AnsiblePassword types.String `tfsdk:"ansible_password"`
216 | AnsiblePrivateKeyFile types.String `tfsdk:"ansible_private_key_file"`
217 | AnsibleSSHCommonArgs types.String `tfsdk:"ansible_ssh_common_args"`
218 | AnsibleSftpExtraArgs types.String `tfsdk:"ansible_sftp_extra_args"`
219 | AnsibleScpExtraArgs types.String `tfsdk:"ansible_scp_extra_args"`
220 | AnsibleSSHExtraArgs types.String `tfsdk:"ansible_ssh_extra_args"`
221 | AnsibleSSHPipelining types.Bool `tfsdk:"ansible_ssh_pipelining"`
222 | AnsibleSSHExecutable types.String `tfsdk:"ansible_ssh_executable"`
223 | AnsibleBecome types.Bool `tfsdk:"ansible_become"`
224 | AnsibleBecomeMethod types.String `tfsdk:"ansible_become_method"`
225 | AnsibleBecomeUser types.String `tfsdk:"ansible_become_user"`
226 | AnsibleBecomePassword types.String `tfsdk:"ansible_become_password"`
227 | AnsibleBecomeExe types.String `tfsdk:"ansible_become_exe"`
228 | AnsibleBecomeFlags types.String `tfsdk:"ansible_become_flags"`
229 | AnsibleShellType types.String `tfsdk:"ansible_shell_type"`
230 | AnsiblePythonInterpreter types.String `tfsdk:"ansible_python_interpreter"`
231 | AnsibleShellExecutable types.String `tfsdk:"ansible_shell_executable"`
232 | }
233 |
234 | type JsonHostModel struct {
235 | AnsibleConnection string `json:"ansible_connection,omitempty"`
236 | AnsibleHost string `json:"ansible_host,omitempty"`
237 | AnsiblePort int64 `json:"ansible_port,omitempty"`
238 | AnsibleUser string `json:"ansible_user,omitempty"`
239 | AnsiblePassword string `json:"ansible_password,omitempty"`
240 | AnsiblePrivateKeyFile string `json:"ansible_private_key_file,omitempty"`
241 | AnsibleSSHCommonArgs string `json:"ansible_ssh_common_args,omitempty"`
242 | AnsibleSftpExtraArgs string `json:"ansible_sftp_extra_args,omitempty"`
243 | AnsibleScpExtraArgs string `json:"ansible_scp_extra_args,omitempty"`
244 | AnsibleSSHExtraArgs string `json:"ansible_ssh_extra_args,omitempty"`
245 | AnsibleSSHPipelining bool `json:"ansible_ssh_pipelining,omitempty"`
246 | AnsibleSSHExecutable string `json:"ansible_ssh_executable,omitempty"`
247 | AnsibleBecome bool `json:"ansible_become,omitempty"`
248 | AnsibleBecomeMethod string `json:"ansible_become_method,omitempty"`
249 | AnsibleBecomeUser string `json:"ansible_become_user,omitempty"`
250 | AnsibleBecomePassword string `json:"ansible_become_password,omitempty"`
251 | AnsibleBecomeExe string `json:"ansible_become_exe,omitempty"`
252 | AnsibleBecomeFlags string `json:"ansible_become_flags,omitempty"`
253 | AnsibleShellType string `json:"ansible_shell_type,omitempty"`
254 | AnsiblePythonInterpreter string `json:"ansible_python_interpreter,omitempty"`
255 | AnsibleShellExecutable string `json:"ansible_shell_executable,omitempty"`
256 | }
257 |
258 | // Schema implements datasource.Resource.
259 | func (i *InventoryDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
260 |
261 | createGroupBlock := func(blockOverrides map[string]schema.Block) schema.ListNestedBlock {
262 | blocks := map[string]schema.Block{
263 | "host": schema.ListNestedBlock{
264 | MarkdownDescription: "Describes an ansible host.",
265 | NestedObject: schema.NestedBlockObject{
266 | Attributes: map[string]schema.Attribute{
267 | "name": schema.StringAttribute{
268 | MarkdownDescription: "Name of the host.",
269 | Required: true,
270 | Optional: false,
271 | },
272 | // See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#behavioral-parameters
273 | "ansible_connection": schema.StringAttribute{
274 | MarkdownDescription: "Specifies the connection type to the host. This can be the name of any Ansible connection plugin. SSH protocol types are ssh or paramiko. The default is ssh.",
275 | Required: false,
276 | Optional: true,
277 | },
278 | "ansible_host": schema.StringAttribute{
279 | MarkdownDescription: "Specifies the resolvable name or IP of the host to connect to, if it is different from the alias (name) you wish to give to it.",
280 | Required: false,
281 | Optional: true,
282 | },
283 | "ansible_port": schema.Int64Attribute{
284 | MarkdownDescription: "The connection port number, if not the default (22 for ssh).",
285 | Required: false,
286 | Optional: true,
287 | },
288 | "ansible_user": schema.StringAttribute{
289 | MarkdownDescription: "The username to use when connecting (logging in) to the host.",
290 | Required: false,
291 | Optional: true,
292 | },
293 | "ansible_password": schema.StringAttribute{
294 | MarkdownDescription: "The password to use when connecting (logging in) to the host.",
295 | Required: false,
296 | Optional: true,
297 | Sensitive: true,
298 | },
299 | "ansible_private_key_file": schema.StringAttribute{
300 | MarkdownDescription: "Private key file used by SSH. This is useful if you use multiple keys and you do not want to use SSH agent.",
301 | Required: false,
302 | Optional: true,
303 | },
304 | "ansible_ssh_common_args": schema.StringAttribute{
305 | MarkdownDescription: "Ansible always appends this setting to the default command line for sftp, scp, and ssh. This is useful for configuring a ``ProxyCommand` for a certain host or group.",
306 | Required: false,
307 | Optional: true,
308 | },
309 | "ansible_sftp_extra_args": schema.StringAttribute{
310 | MarkdownDescription: "Extra arguments to pass to the sftp command.",
311 | Required: false,
312 | Optional: true,
313 | },
314 | "ansible_scp_extra_args": schema.StringAttribute{
315 | MarkdownDescription: "Extra arguments to pass to the scp command.",
316 | Required: false,
317 | Optional: true,
318 | },
319 | "ansible_ssh_extra_args": schema.StringAttribute{
320 | MarkdownDescription: "Extra arguments to pass to the ssh command.",
321 | Required: false,
322 | Optional: true,
323 | },
324 | "ansible_ssh_pipelining": schema.BoolAttribute{
325 | MarkdownDescription: "Enable pipelining for SSH connections.",
326 | Required: false,
327 | Optional: true,
328 | },
329 | "ansible_ssh_executable": schema.StringAttribute{
330 | MarkdownDescription: "Specify the SSH executable to use.",
331 | Required: false,
332 | Optional: true,
333 | },
334 | "ansible_become": schema.BoolAttribute{
335 | MarkdownDescription: "Allows you to force privilege escalation.",
336 | Required: false,
337 | Optional: true,
338 | },
339 | "ansible_become_method": schema.StringAttribute{
340 | MarkdownDescription: "Allows you to set the privilege escalation method to a matching become plugin.",
341 | Required: false,
342 | Optional: true,
343 | },
344 | "ansible_become_user": schema.StringAttribute{
345 | MarkdownDescription: "Allows you to set the user you become through privilege escalation.",
346 | Required: false,
347 | Optional: true,
348 | },
349 | "ansible_become_password": schema.StringAttribute{
350 | MarkdownDescription: "Allows you to set the privilege escalation password.",
351 | Required: false,
352 | Optional: true,
353 | Sensitive: true,
354 | },
355 | "ansible_become_exe": schema.StringAttribute{
356 | MarkdownDescription: "Allows you to set the executable for the escalation method you selected.",
357 | Required: false,
358 | Optional: true,
359 | },
360 | "ansible_become_flags": schema.StringAttribute{
361 | MarkdownDescription: "Allows you to set the flags passed to the selected escalation method",
362 | Required: false,
363 | Optional: true,
364 | },
365 | "ansible_shell_type": schema.StringAttribute{
366 | MarkdownDescription: "Specifies the shell type of the target system. You should not use this setting unless you have set the `ansible_shell_executable` to a non-Bourne (sh) compatible shell. By default, Ansible formats commands using sh-style syntax. If you set this to csh or fish, commands that Ansible executes on target systems follow those shell’s syntax instead.",
367 | Required: false,
368 | Optional: true,
369 | },
370 | "ansible_python_interpreter": schema.StringAttribute{
371 | MarkdownDescription: "Allows you to set the Python interpreter to use for the target system.",
372 | Required: false,
373 | Optional: true,
374 | },
375 | "ansible_shell_executable": schema.StringAttribute{
376 | MarkdownDescription: "Allows you to set the shell executable to use for the target system.",
377 | Required: false,
378 | Optional: true,
379 | },
380 | },
381 | },
382 | },
383 | }
384 | maps.Copy(blocks, blockOverrides)
385 | return schema.ListNestedBlock{
386 | MarkdownDescription: "Describes an ansible group.",
387 | NestedObject: schema.NestedBlockObject{
388 | Attributes: map[string]schema.Attribute{
389 | "name": schema.StringAttribute{
390 | MarkdownDescription: "Name of the group.",
391 | Required: true,
392 | },
393 | "vars": schema.MapAttribute{
394 | MarkdownDescription: "Variables to be set for the group.",
395 | Required: false,
396 | Optional: true,
397 | ElementType: types.StringType,
398 | },
399 | },
400 | Blocks: blocks,
401 | },
402 | }
403 | }
404 |
405 | // This schema is nested, we will support 3 levels of nesting.
406 | thirdLevelGroupBlock := createGroupBlock(map[string]schema.Block{})
407 | secondLevelGroupBlock := createGroupBlock(map[string]schema.Block{
408 | "group": thirdLevelGroupBlock,
409 | })
410 | firstLevelGroupBlock := createGroupBlock(map[string]schema.Block{
411 | "group": secondLevelGroupBlock,
412 | })
413 |
414 | resp.Schema = schema.Schema{
415 | MarkdownDescription: "This data source represents an ansible inventory. It has a json attribute containing the JSON representation of the inventory.",
416 | Attributes: map[string]schema.Attribute{
417 | "json": schema.StringAttribute{
418 | MarkdownDescription: "The JSON content of the inventory file.",
419 | Required: false,
420 | Optional: false,
421 | Computed: true,
422 | Sensitive: true, // Might contain sensitive info
423 | },
424 | },
425 | Blocks: map[string]schema.Block{
426 | "group": firstLevelGroupBlock,
427 | },
428 | }
429 | }
430 |
431 | // Read implements datasource.Resource.
432 | func (i *InventoryDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
433 | var plan InventoryDataSourceModel
434 |
435 | resp.Diagnostics.Append(req.Config.Get(ctx, &plan)...)
436 | if resp.Diagnostics.HasError() {
437 | return
438 | }
439 |
440 | fileContent, toJsonDiags := inventoryToJson(&plan)
441 | resp.Diagnostics.Append(toJsonDiags...)
442 | if resp.Diagnostics.HasError() {
443 | return
444 | }
445 |
446 | plan.Json = types.StringValue(string(fileContent))
447 | resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
448 | if resp.Diagnostics.HasError() {
449 | return
450 | }
451 | }
452 |
--------------------------------------------------------------------------------
/provider/resource_playbook.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "os/exec"
8 | "strings"
9 | "time"
10 |
11 | "github.com/ansible/terraform-provider-ansible/providerutils"
12 | "github.com/hashicorp/terraform-plugin-log/tflog"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
15 | )
16 |
17 | const ansiblePlaybook = "ansible-playbook"
18 |
19 | const resourceTimeout = 60
20 |
21 | func resourcePlaybook() *schema.Resource {
22 | return &schema.Resource{
23 | CreateContext: resourcePlaybookCreate,
24 | ReadContext: resourcePlaybookRead,
25 | UpdateContext: resourcePlaybookUpdate,
26 | DeleteContext: resourcePlaybookDelete,
27 |
28 | Schema: map[string]*schema.Schema{
29 | // Required settings
30 | "playbook": {
31 | Type: schema.TypeString,
32 | Required: true,
33 | Optional: false,
34 | Description: "Path to ansible playbook.",
35 | },
36 |
37 | // Optional settings
38 | "ansible_playbook_binary": {
39 | Type: schema.TypeString,
40 | Required: false,
41 | Optional: true,
42 | Default: "ansible-playbook",
43 | Description: "Path to ansible-playbook executable (binary).",
44 | },
45 |
46 | "name": {
47 | Type: schema.TypeString,
48 | Required: true,
49 | Optional: false,
50 | Description: "Name of the desired host on which the playbook will be executed.",
51 | },
52 |
53 | "groups": {
54 | Type: schema.TypeList,
55 | Elem: &schema.Schema{Type: schema.TypeString},
56 | Required: false,
57 | Optional: true,
58 | Description: "List of desired groups of hosts on which the playbook will be executed.",
59 | },
60 |
61 | "replayable": {
62 | Type: schema.TypeBool,
63 | Required: false,
64 | Optional: true,
65 | Default: true,
66 | Description: "" +
67 | "If 'true', the playbook will be executed on every 'terraform apply' and with that, the resource" +
68 | " will be recreated. " +
69 | "If 'false', the playbook will be executed only on the first 'terraform apply'. " +
70 | "Note, that if set to 'true', when doing 'terraform destroy', it might not show in the destroy " +
71 | "output, even though the resource still gets destroyed.",
72 | },
73 |
74 | "ignore_playbook_failure": {
75 | Type: schema.TypeBool,
76 | Required: false,
77 | Optional: true,
78 | Default: false,
79 | Description: "This parameter is good for testing. " +
80 | "Set to 'true' if the desired playbook is meant to fail, " +
81 | "but still want the resource to run successfully.",
82 | },
83 |
84 | // ansible execution commands
85 | "verbosity": { // verbosity is between = (0, 6)
86 | Type: schema.TypeInt,
87 | Required: false,
88 | Optional: true,
89 | Default: 0,
90 | Description: "A verbosity level between 0 and 6. " +
91 | "Set ansible 'verbose' parameter, which causes Ansible to print more debug messages. " +
92 | "The higher the 'verbosity', the more debug details will be printed.",
93 | },
94 |
95 | "tags": {
96 | Type: schema.TypeList,
97 | Elem: &schema.Schema{Type: schema.TypeString},
98 | Required: false,
99 | Optional: true,
100 | Description: "List of tags of plays and tasks to run.",
101 | },
102 |
103 | "limit": {
104 | Type: schema.TypeList,
105 | Elem: &schema.Schema{Type: schema.TypeString},
106 | Required: false,
107 | Optional: true,
108 | Description: "List of hosts to include in playbook execution.",
109 | },
110 |
111 | "check_mode": {
112 | Type: schema.TypeBool,
113 | Required: false,
114 | Optional: true,
115 | Default: false,
116 | Description: "If 'true', playbook execution won't make any changes but " +
117 | "only change predictions will be made.",
118 | },
119 |
120 | "diff_mode": {
121 | Type: schema.TypeBool,
122 | Required: false,
123 | Optional: true,
124 | Default: false,
125 | Description: "" +
126 | "If 'true', when changing (small) files and templates, differences in those files will be shown. " +
127 | "Recommended usage with 'check_mode'.",
128 | },
129 |
130 | // connection configs are handled with extra_vars
131 | "force_handlers": {
132 | Type: schema.TypeBool,
133 | Required: false,
134 | Optional: true,
135 | Default: false,
136 | Description: "If 'true', run handlers even if a task fails.",
137 | },
138 |
139 | // become configs are handled with extra_vars --> these are also connection configs
140 | "extra_vars": {
141 | Type: schema.TypeMap,
142 | Elem: &schema.Schema{Type: schema.TypeString},
143 | Required: false,
144 | Optional: true,
145 | Description: "A map of additional variables as: { key-1 = value-1, key-2 = value-2, ... }.",
146 | },
147 |
148 | "var_files": { // adds @ at the beginning of filename
149 | Type: schema.TypeList,
150 | Elem: &schema.Schema{Type: schema.TypeString},
151 | Required: false,
152 | Optional: true,
153 | Description: "List of variable files.",
154 | },
155 |
156 | // Ansible Vault
157 | "vault_files": {
158 | Type: schema.TypeList,
159 | Elem: &schema.Schema{Type: schema.TypeString},
160 | Required: false,
161 | Optional: true,
162 | Description: "List of vault files.",
163 | },
164 |
165 | "vault_password_file": {
166 | Type: schema.TypeString,
167 | Required: false,
168 | Optional: true,
169 | Default: "",
170 | Description: "Path to a vault password file.",
171 | },
172 |
173 | "vault_id": {
174 | Type: schema.TypeString,
175 | Required: false,
176 | Optional: true,
177 | Default: "",
178 | Description: "ID of the desired vault(s).",
179 | },
180 |
181 | // computed
182 | // debug output
183 | "args": {
184 | Type: schema.TypeList,
185 | Elem: &schema.Schema{Type: schema.TypeString},
186 | Computed: true,
187 | Description: "Used to build arguments to run Ansible playbook with.",
188 | },
189 |
190 | "temp_inventory_file": {
191 | Type: schema.TypeString,
192 | Computed: true,
193 | Description: "Path to created temporary inventory file.",
194 | },
195 |
196 | "ansible_playbook_stdout": {
197 | Type: schema.TypeString,
198 | Computed: true,
199 | Description: "An ansible-playbook CLI stdout output.",
200 | },
201 |
202 | "ansible_playbook_stderr": {
203 | Type: schema.TypeString,
204 | Computed: true,
205 | Description: "An ansible-playbook CLI stderr output.",
206 | },
207 | },
208 | Timeouts: &schema.ResourceTimeout{
209 | Create: schema.DefaultTimeout(resourceTimeout * time.Minute),
210 | },
211 | }
212 | }
213 |
214 | //nolint:maintidx
215 | func resourcePlaybookCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
216 | var diags diag.Diagnostics
217 | // required settings
218 | playbook, okay := data.Get("playbook").(string)
219 | if !okay {
220 | diags = append(diags, diag.Diagnostic{
221 | Severity: diag.Error,
222 | Summary: "ERROR [%s]: couldn't get 'playbook'!",
223 | Detail: ansiblePlaybook,
224 | })
225 | }
226 |
227 | // optional settings
228 | name, okay := data.Get("name").(string)
229 | if !okay {
230 | diags = append(diags, diag.Diagnostic{
231 | Severity: diag.Error,
232 | Summary: "ERROR [%s]: couldn't get 'name'!",
233 | Detail: ansiblePlaybook,
234 | })
235 | }
236 |
237 | verbosity, okay := data.Get("verbosity").(int)
238 | if !okay {
239 | diags = append(diags, diag.Diagnostic{
240 | Severity: diag.Error,
241 | Summary: "ERROR [%s]: couldn't get 'verbosity'!",
242 | Detail: ansiblePlaybook,
243 | })
244 | }
245 |
246 | tags, okay := data.Get("tags").([]interface{})
247 | if !okay {
248 | diags = append(diags, diag.Diagnostic{
249 | Severity: diag.Error,
250 | Summary: "ERROR [%s]: couldn't get 'tags'!",
251 | Detail: ansiblePlaybook,
252 | })
253 | }
254 |
255 | limit, okay := data.Get("limit").([]interface{})
256 | if !okay {
257 | diags = append(diags, diag.Diagnostic{
258 | Severity: diag.Error,
259 | Summary: "ERROR [%s]: couldn't get 'limit'!",
260 | Detail: ansiblePlaybook,
261 | })
262 | }
263 |
264 | checkMode, okay := data.Get("check_mode").(bool)
265 | if !okay {
266 | diags = append(diags, diag.Diagnostic{
267 | Severity: diag.Error,
268 | Summary: "ERROR [%s]: couldn't get 'check_mode'!",
269 | Detail: ansiblePlaybook,
270 | })
271 | }
272 |
273 | diffMode, okay := data.Get("diff_mode").(bool)
274 | if !okay {
275 | diags = append(diags, diag.Diagnostic{
276 | Severity: diag.Error,
277 | Summary: "ERROR [%s]: couldn't get 'diff_mode'!",
278 | Detail: ansiblePlaybook,
279 | })
280 | }
281 |
282 | forceHandlers, okay := data.Get("force_handlers").(bool)
283 | if !okay {
284 | diags = append(diags, diag.Diagnostic{
285 | Severity: diag.Error,
286 | Summary: "ERROR [%s]: couldn't get 'force_handlers'!",
287 | Detail: ansiblePlaybook,
288 | })
289 | }
290 |
291 | extraVars, okay := data.Get("extra_vars").(map[string]interface{})
292 | if !okay {
293 | diags = append(diags, diag.Diagnostic{
294 | Severity: diag.Error,
295 | Summary: "ERROR [%s]: couldn't get 'extra_vars'!",
296 | Detail: ansiblePlaybook,
297 | })
298 | }
299 |
300 | varFiles, okay := data.Get("var_files").([]interface{})
301 | if !okay {
302 | diags = append(diags, diag.Diagnostic{
303 | Severity: diag.Error,
304 | Summary: "ERROR [%s]: couldn't get 'var_files'!",
305 | Detail: ansiblePlaybook,
306 | })
307 | }
308 |
309 | vaultFiles, okay := data.Get("vault_files").([]interface{})
310 | if !okay {
311 | diags = append(diags, diag.Diagnostic{
312 | Severity: diag.Error,
313 | Summary: "ERROR [%s]: couldn't get 'vault_files'!",
314 | Detail: ansiblePlaybook,
315 | })
316 | }
317 |
318 | vaultPasswordFile, okay := data.Get("vault_password_file").(string)
319 | if !okay {
320 | diags = append(diags, diag.Diagnostic{
321 | Severity: diag.Error,
322 | Summary: "ERROR [%s]: couldn't get 'vault_password_file'!",
323 | Detail: ansiblePlaybook,
324 | })
325 | }
326 |
327 | vaultID, okay := data.Get("vault_id").(string)
328 | if !okay {
329 | diags = append(diags, diag.Diagnostic{
330 | Severity: diag.Error,
331 | Summary: "ERROR [%s]: couldn't get 'vault_id'!",
332 | Detail: ansiblePlaybook,
333 | })
334 | }
335 |
336 | // Generate ID
337 | data.SetId(time.Now().String())
338 |
339 | /********************
340 | * PREP THE OPTIONS (ARGS)
341 | */
342 | args := []string{}
343 |
344 | verbose := providerutils.CreateVerboseSwitch(verbosity)
345 | if verbose != "" {
346 | args = append(args, verbose)
347 | }
348 |
349 | if forceHandlers {
350 | args = append(args, "--force-handlers")
351 | }
352 |
353 | args = append(args, "-e", "hostname="+name)
354 |
355 | if len(tags) > 0 {
356 | tmpTags := []string{}
357 |
358 | for _, tag := range tags {
359 | tagStr, okay := tag.(string)
360 | if !okay {
361 | diags = append(diags, diag.Diagnostic{
362 | Severity: diag.Error,
363 | Summary: "ERROR [%s]: couldn't assert type: string",
364 | Detail: ansiblePlaybook,
365 | })
366 | }
367 |
368 | tmpTags = append(tmpTags, tagStr)
369 | }
370 |
371 | tagsStr := strings.Join(tmpTags, ",")
372 | args = append(args, "--tags", tagsStr)
373 | }
374 |
375 | if len(limit) > 0 {
376 | tmpLimit := []string{}
377 |
378 | for _, l := range limit {
379 | limitStr, okay := l.(string)
380 | if !okay {
381 | diags = append(diags, diag.Diagnostic{
382 | Severity: diag.Error,
383 | Summary: "ERROR [%s]: couldn't assert type: string",
384 | Detail: ansiblePlaybook,
385 | })
386 | }
387 |
388 | tmpLimit = append(tmpLimit, limitStr)
389 | }
390 |
391 | limitStr := strings.Join(tmpLimit, ",")
392 | args = append(args, "--limit", limitStr)
393 | }
394 |
395 | if checkMode {
396 | args = append(args, "--check")
397 | }
398 |
399 | if diffMode {
400 | args = append(args, "--diff")
401 | }
402 |
403 | if len(varFiles) != 0 {
404 | for _, varFile := range varFiles {
405 | varFileString, okay := varFile.(string)
406 | if !okay {
407 | diags = append(diags, diag.Diagnostic{
408 | Severity: diag.Error,
409 | Summary: "ERROR [%s]: couldn't assert type: string",
410 | Detail: ansiblePlaybook,
411 | })
412 | }
413 |
414 | args = append(args, "-e", "@"+varFileString)
415 | }
416 | }
417 |
418 | // Ansible vault
419 | if len(vaultFiles) != 0 {
420 | for _, vaultFile := range vaultFiles {
421 | vaultFileString, okay := vaultFile.(string)
422 | if !okay {
423 | diags = append(diags, diag.Diagnostic{
424 | Severity: diag.Error,
425 | Summary: "ERROR [%s]: couldn't assert type: string",
426 | Detail: ansiblePlaybook,
427 | })
428 | }
429 |
430 | args = append(args, "-e", "@"+vaultFileString)
431 | }
432 |
433 | args = append(args, "--vault-id")
434 |
435 | vaultIDArg := ""
436 | if vaultID != "" {
437 | vaultIDArg += vaultID
438 | }
439 |
440 | if vaultPasswordFile != "" {
441 | vaultIDArg += "@" + vaultPasswordFile
442 | } else {
443 | diags = append(diags, diag.Diagnostic{
444 | Severity: diag.Error,
445 | Summary: "ERROR [ansible-playbook]: can't access vault file(s)! Missing 'vault_password_file'!",
446 | Detail: ansiblePlaybook,
447 | })
448 | }
449 |
450 | args = append(args, vaultIDArg)
451 | }
452 |
453 | if len(extraVars) != 0 {
454 | for key, val := range extraVars {
455 | tmpVal, okay := val.(string)
456 | if !okay {
457 | diags = append(diags, diag.Diagnostic{
458 | Severity: diag.Error,
459 | Summary: "ERROR [ansible-playbook]: couldn't assert type: string",
460 | Detail: ansiblePlaybook,
461 | })
462 | }
463 |
464 | args = append(args, "-e", fmt.Sprintf("%s='%s'", key, tmpVal))
465 | }
466 | }
467 |
468 | args = append(args, playbook)
469 |
470 | // set up the args
471 | log.Print("[ANSIBLE ARGS]:")
472 | log.Print(args)
473 |
474 | if err := data.Set("args", args); err != nil {
475 | diags = append(diags, diag.Diagnostic{
476 | Severity: diag.Error,
477 | Summary: fmt.Sprintf("ERROR [ansible-playbook]: couldn't set 'args'! %v", err),
478 | Detail: ansiblePlaybook,
479 | })
480 | }
481 |
482 | if err := data.Set("temp_inventory_file", ""); err != nil {
483 | diags = append(diags, diag.Diagnostic{
484 | Severity: diag.Error,
485 | Summary: fmt.Sprintf("ERROR [ansible-playbook]: couldn't set 'temp_inventory_file'! %v", err),
486 | Detail: ansiblePlaybook,
487 | })
488 | }
489 |
490 | diagsFromUpdate := resourcePlaybookUpdate(ctx, data, meta)
491 | diags = append(diags, diagsFromUpdate...)
492 |
493 | return diags
494 | }
495 |
496 | func resourcePlaybookRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
497 | var diags diag.Diagnostics
498 |
499 | replayable, okay := data.Get("replayable").(bool)
500 |
501 | if !okay {
502 | diags = append(diags, diag.Diagnostic{
503 | Severity: diag.Error,
504 | Summary: "ERROR [%s]: couldn't get 'replayable'!",
505 | Detail: ansiblePlaybook,
506 | })
507 | }
508 | // if (replayable == true) --> then we want to recreate (reapply) this resource: exits == false
509 | // if (replayable == false) --> we don't want to recreate (reapply) this resource: exists == true
510 | if replayable {
511 | // make sure to do destroy of this resource.
512 | resourcePlaybookDelete(ctx, data, meta)
513 | }
514 |
515 | return diags
516 | }
517 |
518 | func resourcePlaybookUpdate(ctx context.Context, data *schema.ResourceData, _ interface{}) diag.Diagnostics {
519 | var diags diag.Diagnostics
520 |
521 | name, okay := data.Get("name").(string)
522 |
523 | if !okay {
524 | diags = append(diags, diag.Diagnostic{
525 | Severity: diag.Error,
526 | Summary: "ERROR [%s]: couldn't get 'name'!",
527 | Detail: ansiblePlaybook,
528 | })
529 | }
530 |
531 | groups, okay := data.Get("groups").([]interface{})
532 | if !okay {
533 | diags = append(diags, diag.Diagnostic{
534 | Severity: diag.Error,
535 | Summary: "ERROR [%s]: couldn't get 'groups'!",
536 | Detail: ansiblePlaybook,
537 | })
538 | }
539 |
540 | ansiblePlaybookBinary, okay := data.Get("ansible_playbook_binary").(string)
541 | if !okay {
542 | diags = append(diags, diag.Diagnostic{
543 | Severity: diag.Error,
544 | Summary: "ERROR [%s]: couldn't get 'ansible_playbook_binary'!",
545 | Detail: ansiblePlaybook,
546 | })
547 | }
548 |
549 | playbook, okay := data.Get("playbook").(string)
550 | if !okay {
551 | diags = append(diags, diag.Diagnostic{
552 | Severity: diag.Error,
553 | Summary: "ERROR [%s]: couldn't get 'playbook'!",
554 | Detail: ansiblePlaybook,
555 | })
556 | }
557 |
558 | tflog.Info(ctx, "LOG [ansible-playbook]: playbook = "+playbook)
559 |
560 | ignorePlaybookFailure, okay := data.Get("ignore_playbook_failure").(bool)
561 | if !okay {
562 | diags = append(diags, diag.Diagnostic{
563 | Severity: diag.Error,
564 | Summary: "ERROR [%s]: couldn't get 'ignore_playbook_failure'!",
565 | Detail: ansiblePlaybook,
566 | })
567 | }
568 |
569 | argsTf, okay := data.Get("args").([]interface{})
570 |
571 | if !okay {
572 | diags = append(diags, diag.Diagnostic{
573 | Severity: diag.Error,
574 | Summary: "ERROR [%s]: couldn't get 'args'!",
575 | Detail: ansiblePlaybook,
576 | })
577 | }
578 |
579 | tempInventoryFile, okay := data.Get("temp_inventory_file").(string)
580 | if !okay {
581 | diags = append(diags, diag.Diagnostic{
582 | Severity: diag.Error,
583 | Summary: "ERROR [%s]: couldn't get 'temp_inventory_file'!",
584 | Detail: ansiblePlaybook,
585 | })
586 | }
587 |
588 | inventoryFileNamePrefix := ".inventory-"
589 |
590 | if tempInventoryFile == "" {
591 | tempFileName, diagsFromUtils := providerutils.BuildPlaybookInventory(
592 | inventoryFileNamePrefix+"*.ini",
593 | name,
594 | -1,
595 | groups,
596 | )
597 | tempInventoryFile = tempFileName
598 |
599 | diags = append(diags, diagsFromUtils...)
600 |
601 | if err := data.Set("temp_inventory_file", tempInventoryFile); err != nil {
602 | diags = append(diags, diag.Diagnostic{
603 | Severity: diag.Error,
604 | Summary: "ERROR [ansible-playbook]: couldn't set 'temp_inventory_file'!",
605 | Detail: ansiblePlaybook,
606 | })
607 | }
608 | }
609 |
610 | tflog.Debug(ctx, "Temp Inventory File: "+tempInventoryFile)
611 |
612 | // ********************************* RUN PLAYBOOK ********************************
613 |
614 | // Validate ansible-playbook binary
615 | if _, validateBinPath := exec.LookPath(ansiblePlaybookBinary); validateBinPath != nil {
616 | diags = append(diags, diag.Diagnostic{
617 | Severity: diag.Error,
618 | Summary: "ERROR [ansible-playbook]: couldn't find executable " + ansiblePlaybookBinary,
619 | })
620 | }
621 |
622 | if diags.HasError() {
623 | return diags
624 | }
625 |
626 | args := []string{}
627 |
628 | args = append(args, "-i", tempInventoryFile)
629 |
630 | for _, arg := range argsTf {
631 | tmpArg, okay := arg.(string)
632 | if !okay {
633 | diags = append(diags, diag.Diagnostic{
634 | Severity: diag.Error,
635 | Summary: "ERROR [ansible-playbook]: couldn't assert type: string",
636 | Detail: ansiblePlaybook,
637 | })
638 | }
639 |
640 | args = append(args, tmpArg)
641 | }
642 |
643 | tflog.Info(ctx, fmt.Sprintf("Running Command <%s %s>", ansiblePlaybookBinary, strings.Join(args, " ")))
644 | runAnsiblePlay := exec.Command(ansiblePlaybookBinary, args...)
645 |
646 | runAnsiblePlayOut, runAnsiblePlayErr := runAnsiblePlay.CombinedOutput()
647 | ansiblePlayStderrString := ""
648 |
649 | if runAnsiblePlayErr != nil {
650 | playbookFailMsg := string(runAnsiblePlayOut)
651 | if !ignorePlaybookFailure {
652 | diags = append(diags, diag.Diagnostic{
653 | Severity: diag.Error,
654 | Summary: playbookFailMsg,
655 | Detail: ansiblePlaybook,
656 | })
657 | } else {
658 | log.Print(playbookFailMsg)
659 | diags = append(diags, diag.Diagnostic{
660 | Severity: diag.Warning,
661 | Summary: playbookFailMsg,
662 | Detail: ansiblePlaybook,
663 | })
664 | }
665 |
666 | ansiblePlayStderrString = runAnsiblePlayErr.Error()
667 | }
668 | // Set the ansible_playbook_stdout to the CLI stdout of call "ansible-playbook" command above
669 | if err := data.Set("ansible_playbook_stdout", string(runAnsiblePlayOut)); err != nil {
670 | diags = append(diags, diag.Diagnostic{
671 | Severity: diag.Error,
672 | Summary: "ERROR [%s]: couldn't set 'ansible_playbook_stdout' ",
673 | Detail: ansiblePlaybook,
674 | })
675 | }
676 |
677 | // Set the ansible_playbook_stderr to the CLI stderr of call "ansible-playbook" command above
678 | if err := data.Set("ansible_playbook_stderr", ansiblePlayStderrString); err != nil {
679 | diags = append(diags, diag.Diagnostic{
680 | Severity: diag.Error,
681 | Summary: "ERROR [%s]: couldn't set 'ansible_playbook_stderr' ",
682 | Detail: ansiblePlaybook,
683 | })
684 | }
685 |
686 | tflog.Debug(ctx, fmt.Sprintf("LOG [ansible-playbook]: %s", runAnsiblePlayOut))
687 |
688 | // Wait for playbook execution to finish, then remove the temporary file
689 | err := runAnsiblePlay.Wait()
690 | if err != nil {
691 | tflog.Error(ctx, fmt.Sprintf("LOG [ansible-playbook]: didn't wait for playbook to execute: %v", err))
692 | }
693 |
694 | diagsFromUtils := providerutils.RemoveFile(tempInventoryFile)
695 |
696 | diags = append(diags, diagsFromUtils...)
697 |
698 | if err := data.Set("temp_inventory_file", ""); err != nil {
699 | diags = append(diags, diag.Diagnostic{
700 | Severity: diag.Error,
701 | Summary: "ERROR [ansible-playbook]: couldn't set 'temp_inventory_file'!",
702 | Detail: ansiblePlaybook,
703 | })
704 | }
705 |
706 | // *******************************************************************************
707 |
708 | // NOTE: Calling `resourcePlaybookRead` will make a call to `resourcePlaybookDelete` which sets
709 | // data.SetId(""), so when replayable is true, the resource gets created and then immediately deleted.
710 | // This causes provider to fail, therefore we essentially can't call data.SetId("") during a create task
711 |
712 | // diagsFromRead := resourcePlaybookRead(ctx, data, meta)
713 | // diags = append(diags, diagsFromRead...)
714 | return diags
715 | }
716 |
717 | // On "terraform destroy", every resource removes its temporary inventory file.
718 | func resourcePlaybookDelete(_ context.Context, data *schema.ResourceData, _ interface{}) diag.Diagnostics {
719 | data.SetId("")
720 |
721 | return nil
722 | }
723 |
--------------------------------------------------------------------------------
/framework/action_playbook_run.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "os"
9 | "os/exec"
10 | "strings"
11 | "time"
12 |
13 | "github.com/ansible/terraform-provider-ansible/providerutils"
14 | "github.com/hashicorp/terraform-plugin-framework/action"
15 | "github.com/hashicorp/terraform-plugin-framework/action/schema"
16 | "github.com/hashicorp/terraform-plugin-framework/path"
17 | "github.com/hashicorp/terraform-plugin-framework/types"
18 | "github.com/hashicorp/terraform-plugin-log/tflog"
19 | )
20 |
21 | var (
22 | _ action.ActionWithValidateConfig = (*runPlaybookRunAction)(nil)
23 | )
24 |
25 | func NewRunPlaybookRunAction() action.Action {
26 | return &runPlaybookRunAction{}
27 | }
28 |
29 | type runPlaybookRunAction struct{}
30 |
31 | func (a *runPlaybookRunAction) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
32 | resp.TypeName = "ansible_playbook_run"
33 | }
34 |
35 | func (a *runPlaybookRunAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
36 | resp.Schema = schema.Schema{
37 | Description: "This action runs the ansible-playbook CLI command.",
38 | Attributes: map[string]schema.Attribute{
39 | // Positional arguments
40 | "playbooks": schema.ListAttribute{
41 | ElementType: types.StringType,
42 | Required: true,
43 | Optional: false,
44 | Description: "Paths to ansible playbooks.",
45 | },
46 |
47 | // Flag arguments
48 | "become_password_file": schema.StringAttribute{
49 | Required: false,
50 | Optional: true,
51 | Description: "Path to file containing password for privilege escalation.",
52 | },
53 |
54 | "connection_password_file": schema.StringAttribute{
55 | Required: false,
56 | Optional: true,
57 | Description: "Path to file containing password for connection.",
58 | },
59 |
60 | "force_handlers": schema.BoolAttribute{
61 | Required: false,
62 | Optional: true,
63 | Description: "Force handlers to run even if a task fails.",
64 | },
65 |
66 | "flush_cache": schema.BoolAttribute{
67 | Required: false,
68 | Optional: true,
69 | Description: "Flush the cache before running the playbook.",
70 | },
71 |
72 | "skip_tags": schema.ListAttribute{
73 | ElementType: types.StringType,
74 | Required: false,
75 | Optional: true,
76 | Description: "List of tags to skip during playbook execution.",
77 | },
78 |
79 | "start_at_task": schema.StringAttribute{
80 | Required: false,
81 | Optional: true,
82 | Description: "Name of task to start execution at.",
83 | },
84 |
85 | "vault_ids": schema.ListAttribute{
86 | ElementType: types.StringType,
87 | Required: false,
88 | Optional: true,
89 | Description: "The vault identities to use",
90 | },
91 |
92 | "vault_password_file": schema.StringAttribute{
93 | Required: false,
94 | Optional: true,
95 | Description: "The vault password file to use",
96 | },
97 |
98 | "check_mode": schema.BoolAttribute{
99 | Required: false,
100 | Optional: true,
101 | Description: "Run in check mode",
102 | },
103 |
104 | "diff_mode": schema.BoolAttribute{
105 | Required: false,
106 | Optional: true,
107 | Description: "Run in diff mode",
108 | },
109 |
110 | "module_paths": schema.ListAttribute{
111 | ElementType: types.StringType,
112 | Required: false,
113 | Optional: true,
114 | Description: "Prepend path(s) to module library",
115 | },
116 |
117 | "extra_vars": schema.MapAttribute{
118 | ElementType: types.StringType,
119 | Required: false,
120 | Optional: true,
121 | Description: "Extra variables to pass to the playbook",
122 | },
123 |
124 | "extra_vars_files": schema.ListAttribute{
125 | ElementType: types.StringType,
126 | Required: false,
127 | Optional: true,
128 | Description: "List of variable files with extra variables",
129 | },
130 |
131 | "forks": schema.Int64Attribute{
132 | Required: false,
133 | Optional: true,
134 | Description: "Number of parallel forks to use",
135 | },
136 |
137 | "inventories": schema.ListAttribute{
138 | ElementType: types.StringType,
139 | Required: false,
140 | Optional: true,
141 | Description: "List of inventories in JSON format (use ansible_inventory to generate)",
142 | },
143 |
144 | "inventory_files": schema.ListAttribute{
145 | ElementType: types.StringType,
146 | Required: false,
147 | Optional: true,
148 | Description: "Specify inventory host path or comma separated host list",
149 | },
150 |
151 | "limit": schema.StringAttribute{
152 | Required: false,
153 | Optional: true,
154 | Description: "Limit the execution to hosts matching a pattern",
155 | },
156 |
157 | "tags": schema.ListAttribute{
158 | ElementType: types.StringType,
159 | Required: false,
160 | Optional: true,
161 | Description: "Limit the execution to tasks matching a tag",
162 | },
163 |
164 | "verbosity": schema.Int32Attribute{
165 | Required: false,
166 | Optional: true,
167 | Description: "Verbosity level",
168 | },
169 |
170 | "private_key_file": schema.StringAttribute{
171 | Required: false,
172 | Optional: true,
173 | Description: "Path to private key file",
174 | },
175 |
176 | "scp_extra_args": schema.StringAttribute{
177 | Required: false,
178 | Optional: true,
179 | Description: "Extra arguments to pass to scp",
180 | },
181 |
182 | "sftp_extra_args": schema.StringAttribute{
183 | Required: false,
184 | Optional: true,
185 | Description: "Extra arguments to pass to sftp",
186 | },
187 |
188 | "ssh_common_args": schema.StringAttribute{
189 | Required: false,
190 | Optional: true,
191 | Description: "Specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand)",
192 | },
193 |
194 | "ssh_extra_args": schema.StringAttribute{
195 | Required: false,
196 | Optional: true,
197 | Description: "Extra arguments to pass to ssh",
198 | },
199 |
200 | "timeout": schema.Int32Attribute{
201 | Required: false,
202 | Optional: true,
203 | Description: "Override the connection timeout in seconds",
204 | },
205 |
206 | "connection_type": schema.StringAttribute{
207 | Required: false,
208 | Optional: true,
209 | Description: "Connection type to use (default=ssh)",
210 | },
211 |
212 | "user": schema.StringAttribute{
213 | Required: false,
214 | Optional: true,
215 | Description: "Connect as this user (default=None)",
216 | },
217 |
218 | "become_user": schema.StringAttribute{
219 | Required: false,
220 | Optional: true,
221 | Description: "Become this user (default=root)",
222 | },
223 |
224 | "become_method": schema.StringAttribute{
225 | Required: false,
226 | Optional: true,
227 | Description: "Privilege escalation method to use (default=sudo), use `ansible-doc -t become -l` to list valid choices.",
228 | },
229 |
230 | "become": schema.BoolAttribute{
231 | Required: false,
232 | Optional: true,
233 | Description: "Run operations with become",
234 | },
235 |
236 | // Terraform Only options
237 | "quiet": schema.BoolAttribute{
238 | Required: false,
239 | Optional: true,
240 | Description: "Suppress output completely",
241 | },
242 |
243 | "ansible_playbook_binary": schema.StringAttribute{
244 | Required: false,
245 | Optional: true,
246 | Description: "Path to ansible-playbook executable (binary).",
247 | },
248 | },
249 | }
250 | }
251 |
252 | type runPlaybookActionModel struct {
253 | Playbooks types.List `tfsdk:"playbooks"`
254 | AnsiblePlaybookBinary types.String `tfsdk:"ansible_playbook_binary"`
255 | BecomePasswordFile types.String `tfsdk:"become_password_file"`
256 | ConnectionPasswordFile types.String `tfsdk:"connection_password_file"`
257 | SkipTags types.List `tfsdk:"skip_tags"`
258 | StartAtTask types.String `tfsdk:"start_at_task"`
259 | VaultIds types.List `tfsdk:"vault_ids"`
260 | VaultPasswordFile types.String `tfsdk:"vault_password_file"`
261 | CheckMode types.Bool `tfsdk:"check_mode"`
262 | DiffMode types.Bool `tfsdk:"diff_mode"`
263 | ModulePaths types.List `tfsdk:"module_paths"`
264 | ExtraVars types.Map `tfsdk:"extra_vars"`
265 | ExtraVarsFiles types.List `tfsdk:"extra_vars_files"`
266 | Forks types.Int64 `tfsdk:"forks"`
267 | Inventories types.List `tfsdk:"inventories"`
268 | InventoryFiles types.List `tfsdk:"inventory_files"`
269 | Limit types.String `tfsdk:"limit"`
270 | Tags types.List `tfsdk:"tags"`
271 | Verbosity types.Int32 `tfsdk:"verbosity"`
272 | Quiet types.Bool `tfsdk:"quiet"`
273 | PrivateKeyFile types.String `tfsdk:"private_key_file"`
274 | ScpExtraArgs types.String `tfsdk:"scp_extra_args"`
275 | SftpExtraArgs types.String `tfsdk:"sftp_extra_args"`
276 | SshCommonArgs types.String `tfsdk:"ssh_common_args"`
277 | SshExtraArgs types.String `tfsdk:"ssh_extra_args"`
278 | Timeout types.Int32 `tfsdk:"timeout"`
279 | ConnectionType types.String `tfsdk:"connection_type"`
280 | User types.String `tfsdk:"user"`
281 | BecomeUser types.String `tfsdk:"become_user"`
282 | BecomeMethod types.String `tfsdk:"become_method"`
283 | Become types.Bool `tfsdk:"become"`
284 | FlushCache types.Bool `tfsdk:"flush_cache"`
285 | ForceHandlers types.Bool `tfsdk:"force_handlers"`
286 | }
287 |
288 | func (a *runPlaybookRunAction) ValidateConfig(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) {
289 | var config runPlaybookActionModel
290 |
291 | resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
292 | if resp.Diagnostics.HasError() {
293 | return
294 | }
295 |
296 | var playbooks []types.String
297 | resp.Diagnostics.Append(config.Playbooks.ElementsAs(ctx, &playbooks, false)...)
298 | if resp.Diagnostics.HasError() {
299 | return
300 | }
301 |
302 | if len(playbooks) == 0 {
303 | resp.Diagnostics.AddError("No playbooks specified", "At least one playbook must be specified")
304 | return
305 | }
306 |
307 | for i, playbook := range playbooks {
308 | if _, err := os.Stat(playbook.ValueString()); os.IsNotExist(err) {
309 | resp.Diagnostics.AddAttributeError(path.Root("playbooks").AtListIndex(i), "playbook not found", fmt.Sprintf("The playbook file %q does not exist: %s", playbook.ValueString(), err.Error()))
310 | }
311 | }
312 |
313 | if !config.Inventories.IsUnknown() {
314 | var inventories []types.String
315 | resp.Diagnostics.Append(config.Inventories.ElementsAs(ctx, &inventories, false)...)
316 | if resp.Diagnostics.HasError() {
317 | return
318 | }
319 | for i, inventory := range inventories {
320 | // Validate all inventories are valid JSON
321 | if !inventory.IsUnknown() && !isJSON(inventory.ValueString()) {
322 | resp.Diagnostics.AddAttributeError(path.Root("inventories").AtListIndex(i), "Invalid JSON", fmt.Sprintf("Expected the inventory to contain valid JSON, got %q", inventory.ValueString()))
323 | }
324 | }
325 | }
326 |
327 | if !config.VaultIds.IsUnknown() && !config.VaultPasswordFile.IsUnknown() {
328 | var vaultFiles []types.String
329 | resp.Diagnostics.Append(config.VaultIds.ElementsAs(ctx, &vaultFiles, false)...)
330 | // We can already do some validations here during plan
331 | if len(vaultFiles) != 0 && config.VaultPasswordFile.ValueString() == "" {
332 | resp.Diagnostics.AddAttributeError(path.Root("vault_password_file"), "vault_password_file is not found", "Can not access vault_files without passing the vault_password_file")
333 | }
334 | }
335 |
336 | if config.BecomePasswordFile.ValueString() != "" {
337 | if _, err := os.Stat(config.BecomePasswordFile.ValueString()); os.IsNotExist(err) {
338 | resp.Diagnostics.AddAttributeError(path.Root("become_password_file"), "become_password_file not found", fmt.Sprintf("The become password file %q does not exist: %s", config.BecomePasswordFile.ValueString(), err.Error()))
339 | }
340 | }
341 |
342 | if config.ConnectionPasswordFile.ValueString() != "" {
343 | if _, err := os.Stat(config.ConnectionPasswordFile.ValueString()); os.IsNotExist(err) {
344 | resp.Diagnostics.AddAttributeError(path.Root("connection_password_file"), "connection_password_file not found", fmt.Sprintf("The connection password file %q does not exist: %s", config.ConnectionPasswordFile.ValueString(), err.Error()))
345 | }
346 | }
347 |
348 | if config.VaultPasswordFile.ValueString() != "" {
349 | if _, err := os.Stat(config.VaultPasswordFile.ValueString()); os.IsNotExist(err) {
350 | resp.Diagnostics.AddAttributeError(path.Root("vault_password_file"), "vault_password_file not found", fmt.Sprintf("The vault password file %q does not exist: %s", config.VaultPasswordFile.ValueString(), err.Error()))
351 | }
352 | }
353 |
354 | if config.PrivateKeyFile.ValueString() != "" {
355 | if _, err := os.Stat(config.PrivateKeyFile.ValueString()); os.IsNotExist(err) {
356 | resp.Diagnostics.AddAttributeError(path.Root("private_key_file"), "private_key_file not found", fmt.Sprintf("The private key file %q does not exist: %s", config.PrivateKeyFile.ValueString(), err.Error()))
357 | }
358 | }
359 |
360 | if !config.ExtraVarsFiles.IsUnknown() {
361 | var extraVarsFiles []types.String
362 | resp.Diagnostics.Append(config.ExtraVarsFiles.ElementsAs(ctx, &extraVarsFiles, false)...)
363 | for i, extraVarsFile := range extraVarsFiles {
364 | if extraVarsFile.ValueString() != "" {
365 | if _, err := os.Stat(extraVarsFile.ValueString()); os.IsNotExist(err) {
366 | resp.Diagnostics.AddAttributeError(
367 | path.Root("extra_vars_files").AtListIndex(i),
368 | fmt.Sprintf("extra_vars_files[%d] not found", i),
369 | fmt.Sprintf(
370 | "The extra vars file %q does not exist: %s",
371 | extraVarsFile.ValueString(),
372 | err.Error(),
373 | ),
374 | )
375 | }
376 | }
377 | }
378 | }
379 | }
380 |
381 | func (a *runPlaybookRunAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
382 | var config runPlaybookActionModel
383 |
384 | resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
385 | if resp.Diagnostics.HasError() {
386 | return
387 | }
388 |
389 | ansiblePlaybookBinary := "ansible-playbook"
390 | if config.AnsiblePlaybookBinary.ValueString() != "" {
391 | ansiblePlaybookBinary = config.AnsiblePlaybookBinary.ValueString()
392 | }
393 |
394 | // Validate ansible-playbook binary
395 | if _, validateBinPath := exec.LookPath(ansiblePlaybookBinary); validateBinPath != nil {
396 | resp.Diagnostics.AddAttributeError(path.Root("ansible_playbook_binary"), "ansible_playbook_binary is not found", fmt.Sprintf("The ansible-playbook binary is not found: %s", validateBinPath))
397 | return
398 | }
399 | /********************
400 | * PREP THE OPTIONS (ARGS)
401 | */
402 | positionalArgs := []string{}
403 |
404 | var playbooks []types.String
405 | resp.Diagnostics.Append(config.Playbooks.ElementsAs(ctx, &playbooks, false)...)
406 | if resp.Diagnostics.HasError() {
407 | return
408 | }
409 |
410 | if len(playbooks) == 0 {
411 | resp.Diagnostics.AddError("No playbooks specified", "At least one playbook must be specified")
412 | return
413 | }
414 |
415 | for _, playbook := range playbooks {
416 | positionalArgs = append(positionalArgs, playbook.ValueString())
417 | }
418 |
419 | flags := []string{}
420 |
421 | verbosityLevel := int(config.Verbosity.ValueInt32())
422 | verbose := providerutils.CreateVerboseSwitch(verbosityLevel)
423 | if verbose != "" {
424 | flags = append(flags, verbose)
425 | }
426 |
427 | becomePasswordFile := config.BecomePasswordFile.ValueString()
428 | if becomePasswordFile != "" {
429 | flags = append(flags, "--become-password-file", becomePasswordFile)
430 | }
431 |
432 | connectionPasswordFile := config.ConnectionPasswordFile.ValueString()
433 | if connectionPasswordFile != "" {
434 | flags = append(flags, "--connection-password-file", connectionPasswordFile)
435 | }
436 |
437 | if config.ForceHandlers.ValueBool() {
438 | flags = append(flags, "--force-handlers")
439 | }
440 |
441 | if config.FlushCache.ValueBool() {
442 | flags = append(flags, "--flush-cache")
443 | }
444 |
445 | var skipTags []types.String
446 | resp.Diagnostics.Append(config.SkipTags.ElementsAs(ctx, &skipTags, false)...)
447 | if resp.Diagnostics.HasError() {
448 | return
449 | }
450 |
451 | for _, tag := range skipTags {
452 | flags = append(flags, "--skip-tags", tag.ValueString())
453 | }
454 |
455 | startAtTask := config.StartAtTask.ValueString()
456 | if startAtTask != "" {
457 | flags = append(flags, "--start-at-task", startAtTask)
458 | }
459 |
460 | var vaultIds []types.String
461 | resp.Diagnostics.Append(config.VaultIds.ElementsAs(ctx, &vaultIds, false)...)
462 | if resp.Diagnostics.HasError() {
463 | return
464 | }
465 |
466 | if len(vaultIds) > 0 {
467 | for _, vaultId := range vaultIds {
468 | flags = append(flags, "--vault-id", vaultId.ValueString())
469 | }
470 | }
471 |
472 | vaultPasswordFile := config.VaultPasswordFile.ValueString()
473 | if vaultPasswordFile != "" {
474 | flags = append(flags, "--vault-password-file", vaultPasswordFile)
475 | }
476 |
477 | if config.CheckMode.ValueBool() {
478 | flags = append(flags, "--check")
479 | }
480 |
481 | if config.DiffMode.ValueBool() {
482 | flags = append(flags, "--diff")
483 | }
484 |
485 | var modulePaths []types.String
486 | resp.Diagnostics.Append(config.ModulePaths.ElementsAs(ctx, &modulePaths, false)...)
487 | if resp.Diagnostics.HasError() {
488 | return
489 | }
490 |
491 | for _, modulePath := range modulePaths {
492 | flags = append(flags, "--module-path", modulePath.ValueString())
493 | }
494 |
495 | var extraVars map[string]string
496 | resp.Diagnostics.Append(config.ExtraVars.ElementsAs(ctx, &extraVars, false)...)
497 | if resp.Diagnostics.HasError() {
498 | return
499 | }
500 |
501 | for key, value := range extraVars {
502 | flags = append(flags, "-e", fmt.Sprintf("%s=%s", key, value))
503 | }
504 |
505 | var extraVarsFiles []types.String
506 | resp.Diagnostics.Append(config.ExtraVarsFiles.ElementsAs(ctx, &extraVarsFiles, false)...)
507 | if resp.Diagnostics.HasError() {
508 | return
509 | }
510 |
511 | for _, extraVarsFile := range extraVarsFiles {
512 | flags = append(flags, "-e", "@"+extraVarsFile.ValueString())
513 | }
514 |
515 | forks := config.Forks.ValueInt64()
516 | if forks != 0 {
517 | flags = append(flags, "--forks", fmt.Sprintf("%d", forks))
518 | }
519 |
520 | var inventoryFiles []types.String
521 | resp.Diagnostics.Append(config.InventoryFiles.ElementsAs(ctx, &inventoryFiles, false)...)
522 | if resp.Diagnostics.HasError() {
523 | return
524 | }
525 |
526 | for _, inventory := range inventoryFiles {
527 | flags = append(flags, "--inventory", inventory.ValueString())
528 | }
529 |
530 | var inventories []types.String
531 | resp.Diagnostics.Append(config.Inventories.ElementsAs(ctx, &inventories, false)...)
532 | if resp.Diagnostics.HasError() {
533 | return
534 | }
535 | for i, inventory := range inventories {
536 | tflog.Warn(ctx, fmt.Sprintf("inventory --> %#v", inventory))
537 | if !isJSON(inventory.ValueString()) {
538 | resp.Diagnostics.AddAttributeError(path.Root("inventories").AtListIndex(i), "Invalid JSON", fmt.Sprintf("Expected the inventory to contain valid JSON, got %q", inventory.ValueString()))
539 | return
540 | }
541 |
542 | tmpInventoryFile, err := os.CreateTemp("", "action_ansible_playbook_run_inventory_*.json")
543 | if err != nil {
544 | resp.Diagnostics.AddAttributeError(path.Root("inventories").AtListIndex(i), "Failed to create temporary inventory file", err.Error())
545 | return
546 | }
547 | defer os.Remove(tmpInventoryFile.Name())
548 |
549 | _, err = tmpInventoryFile.WriteString(inventory.ValueString())
550 | if err != nil {
551 | resp.Diagnostics.AddAttributeError(path.Root("inventories").AtListIndex(i), "Failed to write temporary inventory file", err.Error())
552 | return
553 | }
554 |
555 | flags = append(flags, "--inventory", tmpInventoryFile.Name())
556 | }
557 |
558 | limit := config.Limit.ValueString()
559 | if limit != "" {
560 | flags = append(flags, "--limit", limit)
561 | }
562 |
563 | var tags []types.String
564 | resp.Diagnostics.Append(config.Tags.ElementsAs(ctx, &tags, false)...)
565 | if resp.Diagnostics.HasError() {
566 | return
567 | }
568 |
569 | for _, tag := range tags {
570 | flags = append(flags, "--tags", tag.ValueString())
571 | }
572 |
573 | privateKeyFile := config.PrivateKeyFile.ValueString()
574 | if privateKeyFile != "" {
575 | flags = append(flags, "--private-key", privateKeyFile)
576 | }
577 |
578 | scpExtraArgs := config.ScpExtraArgs.ValueString()
579 | if scpExtraArgs != "" {
580 | flags = append(flags, "--scp-extra-args", scpExtraArgs)
581 | }
582 |
583 | sftpExtraArgs := config.SftpExtraArgs.ValueString()
584 | if sftpExtraArgs != "" {
585 | flags = append(flags, "--sftp-extra-args", sftpExtraArgs)
586 | }
587 |
588 | sshCommonArgs := config.SshCommonArgs.ValueString()
589 | if sshCommonArgs != "" {
590 | flags = append(flags, "--ssh-common-args", sshCommonArgs)
591 | }
592 |
593 | sshExtraArgs := config.SshExtraArgs.ValueString()
594 | if sshExtraArgs != "" {
595 | flags = append(flags, "--ssh-extra-args", sshExtraArgs)
596 | }
597 |
598 | timeout := config.Timeout.ValueInt32()
599 | if timeout != 0 {
600 | flags = append(flags, "--timeout", fmt.Sprintf("%d", timeout))
601 | }
602 |
603 | connection := config.ConnectionType.ValueString()
604 | if connection != "" {
605 | flags = append(flags, "--connection", connection)
606 | }
607 |
608 | user := config.User.ValueString()
609 | if user != "" {
610 | flags = append(flags, "--user", user)
611 | }
612 |
613 | becomeUser := config.BecomeUser.ValueString()
614 | if becomeUser != "" {
615 | flags = append(flags, "--become-user", becomeUser)
616 | }
617 |
618 | becomeMethod := config.BecomeMethod.ValueString()
619 | if becomeMethod != "" {
620 | flags = append(flags, "--become-method", becomeMethod)
621 | }
622 |
623 | if config.Become.ValueBool() {
624 | flags = append(flags, "--become")
625 | }
626 |
627 | args := append(flags, positionalArgs...)
628 |
629 | tflog.Info(ctx, fmt.Sprintf("Running Command <%s %s>", ansiblePlaybookBinary, strings.Join(args, " ")))
630 |
631 | cmd := exec.CommandContext(ctx, ansiblePlaybookBinary, args...)
632 |
633 | var stderr strings.Builder
634 | cmd.Stderr = &stderr
635 | cmd.Stdout = &TerraformUiWriter{
636 | send: func(s string) {
637 | if !config.Quiet.ValueBool() {
638 | resp.SendProgress(action.InvokeProgressEvent{
639 | Message: fmt.Sprintf("ansible-playbook: %s", s),
640 | })
641 | }
642 | },
643 | }
644 |
645 | if !config.Quiet.ValueBool() {
646 | resp.SendProgress(action.InvokeProgressEvent{
647 | Message: fmt.Sprintf("Running %s", cmd.String()),
648 | })
649 | }
650 |
651 | err := cmd.Run()
652 |
653 | stderrStr := stderr.String()
654 | if err != nil {
655 | if len(stderrStr) > 0 {
656 | resp.Diagnostics.AddError(
657 | "ansible-playbook failed",
658 | stderrStr,
659 | )
660 | return
661 | }
662 |
663 | resp.Diagnostics.AddError(
664 | "Failed to execute ansible-playbook",
665 | err.Error(),
666 | )
667 | return
668 | }
669 | }
670 |
671 | type TerraformUiWriter struct {
672 | send func(s string)
673 | buffer string
674 | closed bool
675 | lastFlush time.Time
676 | }
677 |
678 | func (t *TerraformUiWriter) Write(p []byte) (n int, err error) {
679 | if t.closed {
680 | return 0, errors.New("Writing on closed writer")
681 | }
682 | t.buffer += string(p)
683 |
684 | now := time.Now()
685 | shouldFlush := false
686 |
687 | if t.lastFlush.IsZero() || now.Sub(t.lastFlush) >= time.Second {
688 | shouldFlush = true
689 | }
690 |
691 | if shouldFlush && len(t.buffer) > 0 {
692 | t.send(t.buffer)
693 | t.buffer = ""
694 | t.lastFlush = now
695 | }
696 |
697 | return len(p), nil
698 | }
699 |
700 | func (t *TerraformUiWriter) Close() error {
701 | if t.closed {
702 | return errors.New("Closing closed writer")
703 | }
704 | t.closed = true
705 | if t.buffer != "" {
706 | t.send(t.buffer)
707 | t.buffer = ""
708 | }
709 | return nil
710 | }
711 |
712 | func isJSON(str string) bool {
713 | var j json.RawMessage
714 | return json.Unmarshal([]byte(str), &j) == nil
715 | }
716 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
4 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
5 | github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo=
6 | github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
7 | github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
8 | github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
9 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
10 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
11 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
12 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
13 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
14 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
15 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
16 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
17 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
18 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
19 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
20 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
21 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
22 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
23 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
24 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
25 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
26 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
27 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
28 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
29 | github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
30 | github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
31 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
32 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
33 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
34 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
35 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
36 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
37 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
39 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
40 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
41 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
42 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
43 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
44 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
45 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
46 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
47 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
48 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
49 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
50 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
51 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
52 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
53 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
54 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
55 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
56 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
57 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
58 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
59 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
60 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
61 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
62 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
63 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
64 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
65 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
66 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
67 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
68 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
69 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
70 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
71 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
72 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
73 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
74 | github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
75 | github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
76 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
77 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
78 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
79 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
80 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
81 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
82 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
83 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
84 | github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0=
85 | github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM=
86 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
87 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
88 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
89 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
90 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
91 | github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
92 | github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
93 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
94 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
95 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
96 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
97 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
98 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
99 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
100 | github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=
101 | github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=
102 | github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
103 | github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
104 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
105 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
106 | github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE=
107 | github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4=
108 | github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU=
109 | github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE=
110 | github.com/hashicorp/terraform-plugin-docs v0.24.0 h1:YNZYd+8cpYclQyXbl1EEngbld8w7/LPOm99GD5nikIU=
111 | github.com/hashicorp/terraform-plugin-docs v0.24.0/go.mod h1:YLg+7LEwVmRuJc0EuCw0SPLxuQXw5mW8iJ5ml/kvi+o=
112 | github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9u9DtyYHyEuhVOfeIXbteWA=
113 | github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
114 | github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
115 | github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
116 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
117 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
118 | github.com/hashicorp/terraform-plugin-mux v0.21.0 h1:QsEYnzSD2c3zT8zUrUGqaFGhV/Z8zRUlU7FY3ZPJFfw=
119 | github.com/hashicorp/terraform-plugin-mux v0.21.0/go.mod h1:Qpt8+6AD7NmL0DS7ASkN0EXpDQ2J/FnnIgeUr1tzr5A=
120 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4=
121 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU=
122 | github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
123 | github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
124 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
125 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
126 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
127 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
128 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
129 | github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
130 | github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
131 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
132 | github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
133 | github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
134 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
135 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
136 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
137 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
138 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
139 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
140 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
141 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
142 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
143 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
144 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
145 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
146 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
147 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
148 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
149 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
150 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
151 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
152 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
153 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
154 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
155 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
156 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
157 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
158 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
159 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
160 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
161 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
162 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
163 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
164 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
165 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
166 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
167 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
168 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
169 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
170 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
171 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
172 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
173 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
174 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
175 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
176 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
177 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
178 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
179 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
180 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
181 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
182 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
183 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
184 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
185 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
186 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
187 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
188 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
189 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
190 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
191 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
192 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
193 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
194 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
195 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
196 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
197 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
198 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
199 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
200 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
201 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
202 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
203 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
204 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
205 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
206 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
207 | github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
208 | github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
209 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
210 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
211 | github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
212 | github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
213 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
214 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
215 | go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
216 | go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU=
217 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
218 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
219 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
220 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
221 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
222 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
223 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
224 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
225 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
226 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
227 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
228 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
229 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
230 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
231 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
232 | golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
233 | golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
234 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
235 | golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
236 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
237 | golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
238 | golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
239 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
240 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
241 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
242 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
243 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
244 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
245 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
246 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
247 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
248 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
249 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
250 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
251 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
252 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
253 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
254 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
255 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
257 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
258 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
259 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
260 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
261 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
262 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
263 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
264 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
265 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
266 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
267 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
268 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
269 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
270 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
271 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
272 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
273 | golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
274 | golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
275 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
276 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
277 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
278 | golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
279 | golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
280 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
281 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
282 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
283 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
284 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
285 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
286 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
287 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
288 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
289 | google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
290 | google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
291 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
292 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
293 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
294 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
295 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
296 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
297 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
298 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
299 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
300 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
301 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
302 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
303 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
304 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
305 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
306 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
307 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
308 |
--------------------------------------------------------------------------------