├── 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 | --------------------------------------------------------------------------------