diff --git a/README.md b/README.md index 4bacc38..9594eff 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ Run Ansible playbooks using Terraform. ```terraform resource "ansible_navigator_run" "webservers_example" { playbook = <<-EOT - - hosts: webservers + - name: Example + hosts: webservers tasks: - - ansible.builtin.package: + - name: Install nginx + ansible.builtin.package: name: nginx EOT inventory = yamlencode({ @@ -22,6 +24,24 @@ resource "ansible_navigator_run" "webservers_example" { } }) } + +data "ansible_navigator_run" "uptime_example" { + playbook = <<-EOT + - name: Example + hosts: all + EOT + inventory = yamlencode({}) + artifact_queries = { + "uptimes" = { + jq_filter = "[.plays[] | select(.name==\"Example\") | .tasks[] | select(.task==\"Gathering Facts\") | {host: .host, uptime_seconds: .res.ansible_facts.ansible_uptime_seconds }]" + } + } +} + +output "uptimes" { + value = jsondecode(data.ansible_navigator_run.uptime_example.artifact_queries.uptimes.results[0]) +} + ``` ## Features @@ -29,7 +49,7 @@ resource "ansible_navigator_run" "webservers_example" { 1. Run Ansible playbooks against Terraform managed infrastructure (without the `local-exec` provisioner). Eliminates the need for additional scripting or pipeline steps. 2. Construct Ansible inventories using other data sources and resources. Set Ansible host and group variables to values and secrets from other providers. 3. Utilize Ansible [execution environments](https://ansible.readthedocs.io/en/latest/getting_started_ee/index.html) (containers images) to customize and run the Ansible software stack. Isolate Ansible and its related dependencies (Python/System packages, collections, etc) to simplify pipeline and workstation setup. -4. Write JSONPath queries against [playbook artifacts](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator). Extract values from the playbook run for use elsewhere in the Terraform configuration. Examples include: Ansible facts, remote file contents, task results -- the possibilities are endless! +4. Write [`jq`](https://jqlang.github.io/jq/) queries against [playbook artifacts](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator). Extract values from the playbook run for use elsewhere in the Terraform configuration. Examples include: Ansible facts, remote file contents, task results -- the possibilities are endless! 5. Control playbook re-run behavior using several "lifecycle" options, including an attribute for running the playbook on resource destruction. Implement conditional plays/tasks with the environment variable `ANSIBLE_TF_OPERATION`. 6. Connect to hosts securely by specifying SSH private keys and known host entries. No need manage `~/.ssh` files or setup `ssh-agent` in the environment which Terraform runs. diff --git a/docs/data-sources/navigator_run.md b/docs/data-sources/navigator_run.md index 1d65fe8..63bafe1 100644 --- a/docs/data-sources/navigator_run.md +++ b/docs/data-sources/navigator_run.md @@ -39,23 +39,22 @@ data "ansible_navigator_run" "inline" { # 2. artifact queries -- get file contents data "ansible_navigator_run" "artifact_query_file" { playbook = <<-EOT - - name: Get file - hosts: all + - name: Example tasks: - - name: resolv.conf + - name: Get file ansible.builtin.slurp: src: /etc/resolv.conf EOT - inventory = "..." + inventory = yamlencode({}) artifact_queries = { "resolv_conf" = { - jsonpath = "$.plays[?(@.__play_name=='Get file')].tasks[?(@.__task=='resolv.conf')].res.content" + jq_filter = ".plays[] | select(.name==\"Example\") | .tasks[] | select(.task==\"Get file\") | .res.content" } } } output "resolv_conf" { - value = base64decode(data.ansible_navigator_run.artifact_query_file.artifact_queries.resolv_conf.result) + value = base64decode(jsondecode(data.ansible_navigator_run.artifact_query_file.artifact_queries.resolv_conf.results[0])) } ``` @@ -71,7 +70,7 @@ output "resolv_conf" { - `ansible_navigator_binary` (String) Path to the `ansible-navigator` binary. By default `$PATH` is searched. - `ansible_options` (Attributes) Ansible [playbook](https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html) run related configuration. (see [below for nested schema](#nestedatt--ansible_options)) -- `artifact_queries` (Attributes Map) Query the playbook artifact with [JSONPath](https://goessner.net/articles/JsonPath/). The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run. (see [below for nested schema](#nestedatt--artifact_queries)) +- `artifact_queries` (Attributes Map) Query the Ansible playbook artifact with [`jq`](https://jqlang.github.io/jq/) syntax. The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run. (see [below for nested schema](#nestedatt--artifact_queries)) - `execution_environment` (Attributes) [Execution environment](https://ansible.readthedocs.io/en/latest/getting_started_ee/index.html) (EE) related configuration. (see [below for nested schema](#nestedatt--execution_environment)) - `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) - `timezone` (String) IANA time zone, use `local` for the system time zone. Defaults to `UTC`. @@ -110,11 +109,11 @@ Required: Required: -- `jsonpath` (String) JSONPath expression. +- `jq_filter` (String) `jq` filter. Example: `.status, .stdout`. Read-Only: -- `result` (String) Result of the query. Result may be empty if a field or map key cannot be located. +- `results` (List of String) Results of the `jq` filter in JSON format. diff --git a/docs/resources/navigator_run.md b/docs/resources/navigator_run.md index 6b0d901..7fc6b62 100644 --- a/docs/resources/navigator_run.md +++ b/docs/resources/navigator_run.md @@ -38,8 +38,8 @@ resource "ansible_navigator_run" "existing" { # 3. configure ansible with ansible.cfg placed in working directory (see example below) resource "ansible_navigator_run" "working_directory" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) working_directory = "some-directory-with-ansible-cfg-file" } @@ -54,7 +54,7 @@ resource "ansible_navigator_run" "environment_variables" { - "{{ lookup('ansible.builtin.env', 'SOME_VAR') }}" - "{{ lookup('ansible.builtin.env', 'EDITOR') }}" EOT - inventory = "..." + inventory = yamlencode({}) execution_environment = { environment_variables_set = { "SOME_VAR" = "some-value" @@ -67,8 +67,8 @@ resource "ansible_navigator_run" "environment_variables" { # 5. ansible playbook options resource "ansible_navigator_run" "ansible_options" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) ansible_options = { force_handlers = true # --force-handlers skip_tags = ["tag1", "tag2"] # --skip-tags tag1,tag2 @@ -89,14 +89,14 @@ resource "ansible_navigator_run" "destroy" { msg: "resource is being destroyed!" when: destroy EOT - inventory = "..." + inventory = yamlencode({}) run_on_destroy = true } # 7. triggers and replacement triggers resource "ansible_navigator_run" "triggers" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) triggers = { somekey = some_resource.example.id # re-run playbook when id changes } @@ -107,17 +107,17 @@ resource "ansible_navigator_run" "triggers" { # 8. artifact queries -- get playbook stdout resource "ansible_navigator_run" "artifact_query_stdout" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) artifact_queries = { "stdout" = { - jsonpath = "$.stdout" + jq_filter = ".stdout" } } } output "playbook_stdout" { - value = join("\n", jsondecode(ansible_navigator_run.artifact_query_stdout.artifact_queries.stdout.result)) + value = join("\n", jsondecode(ansible_navigator_run.artifact_query_stdout.artifact_queries.stdout.results[0])) } # 9. ssh private keys @@ -126,8 +126,8 @@ resource "tls_private_key" "client" { } resource "ansible_navigator_run" "private_keys" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) ansible_options = { private_keys = [ { @@ -144,7 +144,7 @@ resource "tls_private_key" "server" { } resource "ansible_navigator_run" "known_hosts" { - playbook = "..." + playbook = "# example" inventory = yamlencode({ all = { vars = { @@ -195,7 +195,7 @@ pipelining=True - `ansible_navigator_binary` (String) Path to the `ansible-navigator` binary. By default `$PATH` is searched. - `ansible_options` (Attributes) Ansible [playbook](https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html) run related configuration. (see [below for nested schema](#nestedatt--ansible_options)) -- `artifact_queries` (Attributes Map) Query the playbook artifact with [JSONPath](https://goessner.net/articles/JsonPath/). The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run. (see [below for nested schema](#nestedatt--artifact_queries)) +- `artifact_queries` (Attributes Map) Query the Ansible playbook artifact with [`jq`](https://jqlang.github.io/jq/) syntax. The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run. (see [below for nested schema](#nestedatt--artifact_queries)) - `execution_environment` (Attributes) [Execution environment](https://ansible.readthedocs.io/en/latest/getting_started_ee/index.html) (EE) related configuration. (see [below for nested schema](#nestedatt--execution_environment)) - `replacement_triggers` (Map of String) Arbitrary map of values that, when changed, will recreate the resource. Similar to `triggers`, but will cause `id` to change. Useful when combined with `run_on_destroy`. - `run_on_destroy` (Boolean) Run playbook on destroy. The environment variable `ANSIBLE_TF_OPERATION` is set to `delete` during the run to allow for conditional plays, tasks, etc. Defaults to `false`. @@ -237,11 +237,11 @@ Required: Required: -- `jsonpath` (String) JSONPath expression. +- `jq_filter` (String) `jq` filter. Example: `.status, .stdout`. Read-Only: -- `result` (String) Result of the query. Result may be empty if a field or map key cannot be located. +- `results` (List of String) Results of the `jq` filter in JSON format. diff --git a/examples/complete/aws/main.tf b/examples/complete/aws/main.tf index f1410a3..9f1831d 100644 --- a/examples/complete/aws/main.tf +++ b/examples/complete/aws/main.tf @@ -168,12 +168,12 @@ resource "ansible_navigator_run" "this" { } artifact_queries = { "stdout" = { - jsonpath = "$.stdout" + jq_filter = ".stdout" } } depends_on = [aws_iam_user_policy.ssh_ssm] } output "playbook_stdout" { - value = join("\n", jsondecode(ansible_navigator_run.this.artifact_queries.stdout.result)) + value = join("\n", jsondecode(ansible_navigator_run.this.artifact_queries.stdout.results[0])) } diff --git a/examples/complete/libvirt/main.tf b/examples/complete/libvirt/main.tf index f58f289..9908d16 100644 --- a/examples/complete/libvirt/main.tf +++ b/examples/complete/libvirt/main.tf @@ -129,11 +129,11 @@ resource "ansible_navigator_run" "this" { } artifact_queries = { "stdout" = { - jsonpath = "$.stdout" + jq_filter = ".stdout" } } } output "playbook_stdout" { - value = join("\n", jsondecode(ansible_navigator_run.this.artifact_queries.stdout.result)) + value = join("\n", jsondecode(ansible_navigator_run.this.artifact_queries.stdout.result[0])) } diff --git a/examples/data-sources/ansible_navigator_run/data-source.tf b/examples/data-sources/ansible_navigator_run/data-source.tf index 6c4a352..02501f6 100644 --- a/examples/data-sources/ansible_navigator_run/data-source.tf +++ b/examples/data-sources/ansible_navigator_run/data-source.tf @@ -22,21 +22,20 @@ data "ansible_navigator_run" "inline" { # 2. artifact queries -- get file contents data "ansible_navigator_run" "artifact_query_file" { playbook = <<-EOT - - name: Get file - hosts: all + - name: Example tasks: - - name: resolv.conf + - name: Get file ansible.builtin.slurp: src: /etc/resolv.conf EOT - inventory = "..." + inventory = yamlencode({}) artifact_queries = { "resolv_conf" = { - jsonpath = "$.plays[?(@.__play_name=='Get file')].tasks[?(@.__task=='resolv.conf')].res.content" + jq_filter = ".plays[] | select(.name==\"Example\") | .tasks[] | select(.task==\"Get file\") | .res.content" } } } output "resolv_conf" { - value = base64decode(data.ansible_navigator_run.artifact_query_file.artifact_queries.resolv_conf.result) + value = base64decode(jsondecode(data.ansible_navigator_run.artifact_query_file.artifact_queries.resolv_conf.results[0])) } diff --git a/examples/resources/ansible_navigator_run/resource.tf b/examples/resources/ansible_navigator_run/resource.tf index df0bdb1..302060f 100644 --- a/examples/resources/ansible_navigator_run/resource.tf +++ b/examples/resources/ansible_navigator_run/resource.tf @@ -23,8 +23,8 @@ resource "ansible_navigator_run" "existing" { # 3. configure ansible with ansible.cfg placed in working directory (see example below) resource "ansible_navigator_run" "working_directory" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) working_directory = "some-directory-with-ansible-cfg-file" } @@ -39,7 +39,7 @@ resource "ansible_navigator_run" "environment_variables" { - "{{ lookup('ansible.builtin.env', 'SOME_VAR') }}" - "{{ lookup('ansible.builtin.env', 'EDITOR') }}" EOT - inventory = "..." + inventory = yamlencode({}) execution_environment = { environment_variables_set = { "SOME_VAR" = "some-value" @@ -52,8 +52,8 @@ resource "ansible_navigator_run" "environment_variables" { # 5. ansible playbook options resource "ansible_navigator_run" "ansible_options" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) ansible_options = { force_handlers = true # --force-handlers skip_tags = ["tag1", "tag2"] # --skip-tags tag1,tag2 @@ -74,14 +74,14 @@ resource "ansible_navigator_run" "destroy" { msg: "resource is being destroyed!" when: destroy EOT - inventory = "..." + inventory = yamlencode({}) run_on_destroy = true } # 7. triggers and replacement triggers resource "ansible_navigator_run" "triggers" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) triggers = { somekey = some_resource.example.id # re-run playbook when id changes } @@ -92,17 +92,17 @@ resource "ansible_navigator_run" "triggers" { # 8. artifact queries -- get playbook stdout resource "ansible_navigator_run" "artifact_query_stdout" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) artifact_queries = { "stdout" = { - jsonpath = "$.stdout" + jq_filter = ".stdout" } } } output "playbook_stdout" { - value = join("\n", jsondecode(ansible_navigator_run.artifact_query_stdout.artifact_queries.stdout.result)) + value = join("\n", jsondecode(ansible_navigator_run.artifact_query_stdout.artifact_queries.stdout.results[0])) } # 9. ssh private keys @@ -111,8 +111,8 @@ resource "tls_private_key" "client" { } resource "ansible_navigator_run" "private_keys" { - playbook = "..." - inventory = "..." + playbook = "# example" + inventory = yamlencode({}) ansible_options = { private_keys = [ { @@ -129,7 +129,7 @@ resource "tls_private_key" "server" { } resource "ansible_navigator_run" "known_hosts" { - playbook = "..." + playbook = "# example" inventory = yamlencode({ all = { vars = { diff --git a/go.mod b/go.mod index 36ac7c9..845d0ba 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,10 @@ require ( github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-testing v1.9.0 + github.com/hashicorp/terraform-plugin-testing v1.10.0 + github.com/itchyny/gojq v0.12.16 golang.org/x/crypto v0.26.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/client-go v0.30.3 ) require ( @@ -42,9 +42,10 @@ require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect - github.com/hashicorp/hc-install v0.7.0 // indirect + github.com/hashicorp/hc-install v0.8.0 // indirect github.com/hashicorp/hcl/v2 v2.21.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.21.0 // indirect @@ -55,9 +56,10 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect + github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect @@ -65,17 +67,19 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yuin/goldmark v1.7.1 // indirect github.com/yuin/goldmark-meta v1.1.0 // indirect - github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.23.0 // indirect diff --git a/go.sum b/go.sum index 65697a4..237246b 100644 --- a/go.sum +++ b/go.sum @@ -86,13 +86,15 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk= -github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= +github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI= +github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU= github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= @@ -115,8 +117,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= -github.com/hashicorp/terraform-plugin-testing v1.9.0 h1:xOsQRqqlHKXpFq6etTxih3ubdK3HVDtfE1IY7Rpd37o= -github.com/hashicorp/terraform-plugin-testing v1.9.0/go.mod h1:fhhVx/8+XNJZTD5o3b4stfZ6+q7z9+lIWigIYdT6/44= +github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw= +github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -128,6 +130,10 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g= +github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= @@ -150,8 +156,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -172,6 +178,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -205,8 +214,8 @@ github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= -github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= @@ -219,8 +228,8 @@ golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn5 golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -290,5 +299,3 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= -k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= diff --git a/internal/provider/helpers_test.go b/internal/provider/helpers_test.go index 0d65a14..0b096fb 100644 --- a/internal/provider/helpers_test.go +++ b/internal/provider/helpers_test.go @@ -182,7 +182,7 @@ func testSSHServer(t *testing.T, clientPublicKey string, serverPrivateKey string } // https://github.com/hashicorp/terraform-provider-random/blob/main/internal/provider/resource_integer_test.go -func testExtractResourceAttr(resourceName string, attributeName string, attributeValue *string) resource.TestCheckFunc { //nolint:unparam +func testExtractResourceAttr(resourceName string, attributeName string, attributeValue *string) resource.TestCheckFunc { return func(s *terraform.State) error { resourceState, ok := s.RootModule().Resources[resourceName] diff --git a/internal/provider/navigator_run_common.go b/internal/provider/navigator_run_common.go index 64bac7c..580d901 100644 --- a/internal/provider/navigator_run_common.go +++ b/internal/provider/navigator_run_common.go @@ -37,8 +37,8 @@ type PrivateKeyModel struct { } type ArtifactQueryModel struct { - JSONPath types.String `tfsdk:"jsonpath"` - Result types.String `tfsdk:"result"` + JQFilter types.String `tfsdk:"jq_filter"` + Results types.List `tfsdk:"results"` } func (ExecutionEnvironmentModel) AttrTypes() map[string]attr.Type { @@ -162,16 +162,16 @@ func (m PrivateKeyModel) Value(ctx context.Context, key *ansible.PrivateKey) dia func (ArtifactQueryModel) AttrTypes() map[string]attr.Type { return map[string]attr.Type{ - "jsonpath": types.StringType, - "result": types.StringType, + "jq_filter": types.StringType, + "results": types.ListType{ElemType: types.StringType}, } } func (m ArtifactQueryModel) Value(ctx context.Context, query *ansible.ArtifactQuery) diag.Diagnostics { var diags diag.Diagnostics - query.JSONPath = m.JSONPath.ValueString() - query.Result = m.Result.ValueString() + query.JQFilter = m.JQFilter.ValueString() + query.Results = []string{} // m.Results always unknown when this function is called return diags } @@ -179,8 +179,11 @@ func (m ArtifactQueryModel) Value(ctx context.Context, query *ansible.ArtifactQu func (m *ArtifactQueryModel) Set(ctx context.Context, query ansible.ArtifactQuery) diag.Diagnostics { var diags diag.Diagnostics - m.JSONPath = types.StringValue(query.JSONPath) - m.Result = types.StringValue(query.Result) + m.JQFilter = types.StringValue(query.JQFilter) + + resultsValue, newDiags := types.ListValueFrom(ctx, types.StringType, query.Results) + diags.Append(newDiags...) + m.Results = resultsValue return diags } diff --git a/internal/provider/navigator_run_data_source.go b/internal/provider/navigator_run_data_source.go index 7e6926a..0e0434a 100644 --- a/internal/provider/navigator_run_data_source.go +++ b/internal/provider/navigator_run_data_source.go @@ -373,21 +373,24 @@ func (d *NavigatorRunDataSource) Schema(ctx context.Context, req datasource.Sche }, }, "artifact_queries": schema.MapNestedAttribute{ - Description: "Query the playbook artifact with JSONPath. The playbook artifact contains detailed information about every play and task, as well as the stdout from the playbook run.", - MarkdownDescription: "Query the playbook artifact with [JSONPath](https://goessner.net/articles/JsonPath/). The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run.", + Description: "Query the Ansible playbook artifact with 'jq' syntax. The playbook artifact contains detailed information about every play and task, as well as the stdout from the playbook run.", + MarkdownDescription: "Query the Ansible playbook artifact with [`jq`](https://jqlang.github.io/jq/) syntax. The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run.", Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "jsonpath": schema.StringAttribute{ - Description: "JSONPath expression.", - Required: true, + "jq_filter": schema.StringAttribute{ + Description: "'jq' filter. Example: '.status, .stdout'.", + MarkdownDescription: "`jq` filter. Example: `.status, .stdout`.", + Required: true, Validators: []validator.String{ - stringIsIsJSONPathExpression(), + stringIsIsJQFilter(), }, }, - "result": schema.StringAttribute{ - Description: "Result of the query. Result may be empty if a field or map key cannot be located.", - Computed: true, + "results": schema.ListAttribute{ // TODO switch to a dynamic attribute when supported as an element in a collection + Description: "Results of the 'jq' filter in JSON format.", + MarkdownDescription: "Results of the `jq` filter in JSON format.", + Computed: true, + ElementType: types.StringType, }, }, }, diff --git a/internal/provider/navigator_run_data_source_test.go b/internal/provider/navigator_run_data_source_test.go index 0bcbb80..b9a9469 100644 --- a/internal/provider/navigator_run_data_source_test.go +++ b/internal/provider/navigator_run_data_source_test.go @@ -35,7 +35,7 @@ func TestAccNavigatorRunDataSource_artifact_queries(t *testing.T) { "file_contents": config.StringVariable(fileContents), }), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestMatchResourceAttr(navigatorRunDataSource, "artifact_queries.stdout.result", regexp.MustCompile("ok=3")), + resource.TestMatchResourceAttr(navigatorRunDataSource, "artifact_queries.stdout.results.0", regexp.MustCompile("ok=3")), testExtractResourceAttr(navigatorRunDataSource, "command", &dataSourceCommand), ), ConfigStateChecks: []statecheck.StateCheck{ @@ -53,7 +53,7 @@ func TestAccNavigatorRunDataSource_artifact_queries(t *testing.T) { }, }, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestMatchResourceAttr(navigatorRunDataSource, "artifact_queries.stdout.result", regexp.MustCompile("ok=3")), + resource.TestMatchResourceAttr(navigatorRunDataSource, "artifact_queries.stdout.results.0", regexp.MustCompile("ok=3")), testExtractResourceAttr(navigatorRunDataSource, "command", &dataSourceCommandUpdate), testCheckAttributeValuesDiffer(&dataSourceCommand, &dataSourceCommandUpdate), ), diff --git a/internal/provider/navigator_run_resource.go b/internal/provider/navigator_run_resource.go index b3c0b63..526779a 100644 --- a/internal/provider/navigator_run_resource.go +++ b/internal/provider/navigator_run_resource.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -370,23 +371,26 @@ func (r *NavigatorRunResource) Schema(ctx context.Context, req resource.SchemaRe }, }, "artifact_queries": schema.MapNestedAttribute{ - Description: "Query the playbook artifact with JSONPath. The playbook artifact contains detailed information about every play and task, as well as the stdout from the playbook run.", - MarkdownDescription: "Query the playbook artifact with [JSONPath](https://goessner.net/articles/JsonPath/). The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run.", + Description: "Query the Ansible playbook artifact with 'jq' syntax. The playbook artifact contains detailed information about every play and task, as well as the stdout from the playbook run.", + MarkdownDescription: "Query the Ansible playbook artifact with [`jq`](https://jqlang.github.io/jq/) syntax. The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run.", Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "jsonpath": schema.StringAttribute{ - Description: "JSONPath expression.", - Required: true, + "jq_filter": schema.StringAttribute{ + Description: "'jq' filter. Example: '.status, .stdout'.", + MarkdownDescription: "`jq` filter. Example: `.status, .stdout`.", + Required: true, Validators: []validator.String{ - stringIsIsJSONPathExpression(), + stringIsIsJQFilter(), }, }, - "result": schema.StringAttribute{ - Description: "Result of the query. Result may be empty if a field or map key cannot be located.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), + "results": schema.ListAttribute{ // TODO switch to a dynamic attribute when supported as an element in a collection + Description: "Results of the 'jq' filter in JSON format.", + MarkdownDescription: "Results of the `jq` filter in JSON format.", + Computed: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), }, }, }, @@ -482,7 +486,7 @@ func (r *NavigatorRunResource) ModifyPlan(ctx context.Context, req resource.Modi resp.Diagnostics.Append(data.ArtifactQueries.ElementsAs(ctx, &artifactQueriesPlanModel, false)...) for name, model := range artifactQueriesPlanModel { - model.Result = types.StringUnknown() + model.Results = types.ListUnknown(types.StringType) artifactQueriesPlanModel[name] = model } diff --git a/internal/provider/navigator_run_resource_errors_test.go b/internal/provider/navigator_run_resource_errors_test.go index 66d941f..38ab15f 100644 --- a/internal/provider/navigator_run_resource_errors_test.go +++ b/internal/provider/navigator_run_resource_errors_test.go @@ -19,7 +19,7 @@ func TestAccNavigatorRunResource_errors(t *testing.T) { }{ { name: "artifact_query", - expected: regexp.MustCompile("Not a valid JSONPath expression"), + expected: regexp.MustCompile("Not a valid JQ filter"), }, { name: "env_var_name_empty", diff --git a/internal/provider/navigator_run_resource_test.go b/internal/provider/navigator_run_resource_test.go index 18e9c8e..a9e82d6 100644 --- a/internal/provider/navigator_run_resource_test.go +++ b/internal/provider/navigator_run_resource_test.go @@ -55,7 +55,7 @@ func TestAccNavigatorRunResource_artifact_queries(t *testing.T) { "file_contents": config.StringVariable(fileContents), }), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestMatchResourceAttr(navigatorRunResource, "artifact_queries.stdout.result", regexp.MustCompile("ok=3")), + resource.TestMatchResourceAttr(navigatorRunResource, "artifact_queries.stdout.results.0", regexp.MustCompile("ok=3")), testExtractResourceAttr(navigatorRunResource, "command", &resourceCommand), ), ConfigStateChecks: []statecheck.StateCheck{ @@ -70,12 +70,12 @@ func TestAccNavigatorRunResource_artifact_queries(t *testing.T) { ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectNonEmptyPlan(), - plancheck.ExpectUnknownValue(navigatorRunResource, tfjsonpath.New("artifact_queries").AtMapKey("file_contents").AtMapKey("result")), + plancheck.ExpectUnknownValue(navigatorRunResource, tfjsonpath.New("artifact_queries").AtMapKey("file_contents").AtMapKey("results")), plancheck.ExpectUnknownValue(navigatorRunResource, tfjsonpath.New("command")), }, }, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestMatchResourceAttr(navigatorRunResource, "artifact_queries.stdout.result", regexp.MustCompile("ok=3")), + resource.TestMatchResourceAttr(navigatorRunResource, "artifact_queries.stdout.results.0", regexp.MustCompile("ok=3")), testExtractResourceAttr(navigatorRunResource, "command", &resourceCommandUpdate), testCheckAttributeValuesDiffer(&resourceCommand, &resourceCommandUpdate), ), @@ -276,10 +276,10 @@ func TestAccNavigatorRunResource_pull_args(t *testing.T) { { Config: testTerraformFile(t, filepath.Join("navigator_run_resource", "pull_args")), ConfigVariables: testConfigVariables(t, config.Variables{ - "pull_arguments": config.ListVariable(config.StringVariable(arg)), + "pull_args": config.ListVariable(config.StringVariable(arg)), }), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestMatchResourceAttr(navigatorRunResource, "artifact_queries.pull_args.result", regexp.MustCompile(arg)), + resource.TestMatchResourceAttr(navigatorRunResource, "artifact_queries.pull_args.results.0", regexp.MustCompile(arg)), ), }, }, @@ -333,6 +333,7 @@ func TestAccNavigatorRunResource_skip_run(t *testing.T) { t.Parallel() var resourceCommand, resourceCommandUpdate string + var queryResult, queryResultUpdate string resource.Test(t, resource.TestCase{ PreCheck: func() { testPreCheck(t) }, @@ -344,7 +345,9 @@ func TestAccNavigatorRunResource_skip_run(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(navigatorRunResource, "id"), resource.TestCheckResourceAttrSet(navigatorRunResource, "command"), + resource.TestCheckResourceAttrSet(navigatorRunResource, "artifact_queries.test.results.0"), testExtractResourceAttr(navigatorRunResource, "command", &resourceCommand), + testExtractResourceAttr(navigatorRunResource, "artifact_queries.test.results.0", &queryResult), ), }, { @@ -360,8 +363,11 @@ func TestAccNavigatorRunResource_skip_run(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(navigatorRunResource, "id"), resource.TestCheckResourceAttrSet(navigatorRunResource, "command"), + resource.TestCheckResourceAttrSet(navigatorRunResource, "artifact_queries.test.results.0"), testExtractResourceAttr(navigatorRunResource, "command", &resourceCommandUpdate), testCheckAttributeValuesEqual(&resourceCommand, &resourceCommandUpdate), + testExtractResourceAttr(navigatorRunResource, "artifact_queries.test.results.0", &queryResultUpdate), + testCheckAttributeValuesEqual(&queryResult, &queryResultUpdate), ), }, }, diff --git a/internal/provider/testdata/navigator_run_data_source/artifact_queries.tf b/internal/provider/testdata/navigator_run_data_source/artifact_queries.tf index e14d4d8..1a28bd4 100644 --- a/internal/provider/testdata/navigator_run_data_source/artifact_queries.tf +++ b/internal/provider/testdata/navigator_run_data_source/artifact_queries.tf @@ -5,27 +5,27 @@ data "ansible_navigator_run" "test" { hosts: localhost become: false tasks: - - name: write file + - name: Write file ansible.builtin.copy: dest: /tmp/test content: ${var.file_contents} - - name: get file + - name: Get file ansible.builtin.slurp: src: /tmp/test EOT inventory = "# localhost" artifact_queries = { - stdout = { - jsonpath = "$.stdout" + "stdout" = { + jq_filter = ".stdout" } - file_contents = { - jsonpath = "$.plays[?(@.__play_name=='Test')].tasks[?(@.__task=='get file')].res.content" + "file_contents" = { + jq_filter = ".plays[] | select(.name==\"Test\") | .tasks[] | select(.task==\"Get file\") | .res.content" } } } output "file_contents" { - value = base64decode(data.ansible_navigator_run.test.artifact_queries.file_contents.result) + value = base64decode(jsondecode((data.ansible_navigator_run.test.artifact_queries.file_contents.results[0]))) } variable "ansible_navigator_binary" { diff --git a/internal/provider/testdata/navigator_run_resource/artifact_queries.tf b/internal/provider/testdata/navigator_run_resource/artifact_queries.tf index be7b551..b70e017 100644 --- a/internal/provider/testdata/navigator_run_resource/artifact_queries.tf +++ b/internal/provider/testdata/navigator_run_resource/artifact_queries.tf @@ -5,27 +5,27 @@ resource "ansible_navigator_run" "test" { hosts: localhost become: false tasks: - - name: write file + - name: Write file ansible.builtin.copy: dest: /tmp/test content: ${var.file_contents} - - name: get file + - name: Get file ansible.builtin.slurp: src: /tmp/test EOT inventory = "# localhost" artifact_queries = { - stdout = { - jsonpath = "$.stdout" + "stdout" = { + jq_filter = ".stdout" } - file_contents = { - jsonpath = "$.plays[?(@.__play_name=='Test')].tasks[?(@.__task=='get file')].res.content" + "file_contents" = { + jq_filter = ".plays[] | select(.name==\"Test\") | .tasks[] | select(.task==\"Get file\") | .res.content" } } } output "file_contents" { - value = base64decode(ansible_navigator_run.test.artifact_queries.file_contents.result) + value = base64decode(jsondecode(ansible_navigator_run.test.artifact_queries.file_contents.results[0])) } variable "ansible_navigator_binary" { diff --git a/internal/provider/testdata/navigator_run_resource/errors/artifact_query.tf b/internal/provider/testdata/navigator_run_resource/errors/artifact_query.tf index da441d7..cc84bc4 100644 --- a/internal/provider/testdata/navigator_run_resource/errors/artifact_query.tf +++ b/internal/provider/testdata/navigator_run_resource/errors/artifact_query.tf @@ -7,7 +7,7 @@ resource "ansible_navigator_run" "test" { inventory = "# localhost" artifact_queries = { "test" = { - jsonpath = "!" + jq_filter = "!" } } } diff --git a/internal/provider/testdata/navigator_run_resource/pull_args.tf b/internal/provider/testdata/navigator_run_resource/pull_args.tf index 1f819bf..f23ead6 100644 --- a/internal/provider/testdata/navigator_run_resource/pull_args.tf +++ b/internal/provider/testdata/navigator_run_resource/pull_args.tf @@ -6,11 +6,11 @@ resource "ansible_navigator_run" "test" { EOT inventory = "# localhost" execution_environment = { - pull_arguments = var.pull_arguments + pull_arguments = var.pull_args } artifact_queries = { - pull_args = { - jsonpath = "$.settings_entries.ansible-navigator.execution-environment.pull.arguments" + "pull_args" = { + jq_filter = ".settings_entries.\"ansible-navigator\".\"execution-environment\".pull.arguments" } } } @@ -20,7 +20,7 @@ variable "ansible_navigator_binary" { nullable = false } -variable "pull_arguments" { +variable "pull_args" { type = list(string) nullable = false } diff --git a/internal/provider/testdata/navigator_run_resource/skip_run.tf b/internal/provider/testdata/navigator_run_resource/skip_run.tf index ac4ab38..11cb239 100644 --- a/internal/provider/testdata/navigator_run_resource/skip_run.tf +++ b/internal/provider/testdata/navigator_run_resource/skip_run.tf @@ -5,7 +5,12 @@ resource "ansible_navigator_run" "test" { become: false EOT inventory = "# localhost" - run_on_destroy = true + artifact_queries = { + "test" = { + jq_filter = "now" + } + } + run_on_destroy = true timeouts = { create = "60m" update = "60m" diff --git a/internal/provider/testdata/navigator_run_resource/skip_run_update.tf b/internal/provider/testdata/navigator_run_resource/skip_run_update.tf index b32a92b..bb5b496 100644 --- a/internal/provider/testdata/navigator_run_resource/skip_run_update.tf +++ b/internal/provider/testdata/navigator_run_resource/skip_run_update.tf @@ -5,7 +5,12 @@ resource "ansible_navigator_run" "test" { become: false EOT inventory = "# localhost" - run_on_destroy = false + artifact_queries = { + "test" = { + jq_filter = "now" + } + } + run_on_destroy = false timeouts = { create = "90m" update = "90m" diff --git a/internal/provider/validators.go b/internal/provider/validators.go index 955f9f6..400e2bf 100644 --- a/internal/provider/validators.go +++ b/internal/provider/validators.go @@ -153,7 +153,7 @@ func (v stringIsYAMLValidator) ValidateString(ctx context.Context, req validator return } - var output interface{} + var output any err := yaml.Unmarshal([]byte(req.ConfigValue.ValueString()), &output) if addPathError(&resp.Diagnostics, req.Path, "Not valid YAML", err) { return @@ -204,25 +204,25 @@ func stringIsIANATimezone() stringIsIANATimezoneValidator { return stringIsIANATimezoneValidator{} } -type stringIsIsJSONPathExpressionValidator struct{} +type stringIsIsJQFilterValidator struct{} -func (v stringIsIsJSONPathExpressionValidator) Description(ctx context.Context) string { - return "string must be a JSONPath expression" +func (v stringIsIsJQFilterValidator) Description(ctx context.Context) string { + return "string must be a JQ filter" } -func (v stringIsIsJSONPathExpressionValidator) MarkdownDescription(ctx context.Context) string { +func (v stringIsIsJQFilterValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -func (v stringIsIsJSONPathExpressionValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { +func (v stringIsIsJQFilterValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { return } - err := ansible.ValidateJSONPathExpression(req.ConfigValue.ValueString()) - addPathError(&resp.Diagnostics, req.Path, "Not a valid JSONPath expression", err) + err := ansible.ValidateJQFilter(req.ConfigValue.ValueString()) + addPathError(&resp.Diagnostics, req.Path, "Not a valid JQ filter", err) } -func stringIsIsJSONPathExpression() stringIsIsJSONPathExpressionValidator { - return stringIsIsJSONPathExpressionValidator{} +func stringIsIsJQFilter() stringIsIsJQFilterValidator { + return stringIsIsJQFilterValidator{} } diff --git a/pkg/ansible/navigator_query.go b/pkg/ansible/navigator_query.go index 8e778a0..4182699 100644 --- a/pkg/ansible/navigator_query.go +++ b/pkg/ansible/navigator_query.go @@ -4,11 +4,13 @@ import ( "fmt" "os" "path/filepath" + + jq "github.com/itchyny/gojq" ) type ArtifactQuery struct { - JSONPath string - Result string + JQFilter string + Results []string } func QueryPlaybookArtifact(dir string, queries map[string]ArtifactQuery) error { @@ -20,20 +22,20 @@ func QueryPlaybookArtifact(dir string, queries map[string]ArtifactQuery) error { } for name, query := range queries { - result, err := jsonPath(contents, query.JSONPath) + results, err := jqJSON(contents, query.JQFilter) if err != nil { - return fmt.Errorf("failed to query playbook artifact with JSONPath, %w", err) + return fmt.Errorf("failed to query playbook artifact, %w", err) } - query.Result = result + query.Results = results queries[name] = query } return nil } -func ValidateJSONPathExpression(expression string) error { - _, err := jsonPathParse(expression) +func ValidateJQFilter(filter string) error { + _, err := jq.Parse(filter) return err } diff --git a/pkg/ansible/utils.go b/pkg/ansible/utils.go index 986b018..37edc2d 100644 --- a/pkg/ansible/utils.go +++ b/pkg/ansible/utils.go @@ -1,13 +1,12 @@ package ansible import ( - "bytes" "encoding/json" - "fmt" + "errors" "os" "os/exec" - "k8s.io/client-go/util/jsonpath" + jq "github.com/itchyny/gojq" ) func programExistsOnPath(program string) error { @@ -22,33 +21,42 @@ func writeFile(path string, contents string) error { return os.WriteFile(path, []byte(contents), 0o600) //nolint:gomnd,mnd } -func jsonPathParse(expression string) (*jsonpath.JSONPath, error) { - jsonPath := jsonpath.New(expression) - jsonPath.AllowMissingKeys(true) +func jqJSON(data []byte, filter string) ([]string, error) { + var blob any + if err := json.Unmarshal(data, &blob); err != nil { + return nil, err + } - err := jsonPath.Parse(fmt.Sprintf("{%s}", expression)) + query, err := jq.Parse(filter) if err != nil { return nil, err } - return jsonPath, nil -} + var results []string -func jsonPath(data []byte, expression string) (string, error) { - var blob interface{} - if err := json.Unmarshal(data, &blob); err != nil { - return "", err - } + iter := query.Run(blob) + for { + value, ok := iter.Next() + if !ok { + break + } - jsonPath, err := jsonPathParse(expression) - if err != nil { - return "", err - } + if err, ok := value.(error); ok { + var haltErr *jq.HaltError + if errors.As(err, &haltErr) && haltErr.Value() == nil { + break + } + + return nil, err + } + + result, err := jq.Marshal(value) + if err != nil { + return nil, err + } - output := new(bytes.Buffer) - if err := jsonPath.Execute(output, blob); err != nil { - return "", err + results = append(results, string(result)) } - return output.String(), nil + return results, nil }