Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow terraform module to specify complex variable structures #4797

Merged
merged 36 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ab08a48
Adding capability to specify complex variables type to terraform
kosalaat Jun 7, 2022
921414a
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 13, 2022
50888d2
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 13, 2022
cbdad6b
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 13, 2022
760e85e
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 13, 2022
8680f57
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 13, 2022
52b1de1
Adding the changelog fragment
kosalaat Jun 14, 2022
0f35b78
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 14, 2022
7252ec1
Adding ``integer_types`` from ``module_utils``
kosalaat Jun 14, 2022
85c58f5
Update changelogs/fragments/4797-terraform-complex-variables.yml
kosalaat Jun 19, 2022
217795c
* Changed to approach to make the code more readble and simple to…
kosalaat Jun 26, 2022
d9d468b
Update plugins/modules/cloud/misc/terraform.py
kosalaat Jun 26, 2022
5a8c29f
adding boolean explicitly, although boolean is a subclass of integer,…
kosalaat Jun 26, 2022
eefbae9
fixing the doc strings
kosalaat Jun 26, 2022
960c902
Update terraform.py
kosalaat Jul 9, 2022
c73f2ac
* Introducing format_args funtion to simplify formatting each argumen…
kosalaat Aug 21, 2022
0b941e4
Update plugins/modules/cloud/misc/terraform.py
kosalaat Aug 22, 2022
1e13d3a
* Adding full terraform command to fail_json() when the terrafor …
kosalaat Aug 23, 2022
7d57fa3
plan_command if a list, stringifying the list
kosalaat Aug 27, 2022
267ff86
* Fixing the new line for the change fragments
kosalaat Sep 7, 2022
24ef92f
Update changelogs/fragments/4797-terraform-complex-variables.yml
kosalaat Sep 19, 2022
ed372b8
Update plugins/modules/cloud/misc/terraform.py
kosalaat Sep 19, 2022
acd61bd
double-quotes are not properly escaped in shell, and python string
kosalaat Sep 20, 2022
dc9ded7
changing all the task actions to FQCN format.
kosalaat Sep 20, 2022
f7b2aa7
integration testing now includes:
kosalaat Sep 20, 2022
eb3bcd6
Adding colon ':' to string test casses.
kosalaat Sep 20, 2022
38b104f
Added complex_vars to switch between the old and the new variable
kosalaat Oct 1, 2022
3dbab72
Added tests for the new escape sequences.
kosalaat Oct 1, 2022
eafc630
Restructuring the documente strings to a shorter string.
kosalaat Oct 1, 2022
0782937
Update changelogs/fragments/4797-terraform-complex-variables.yml
kosalaat Oct 1, 2022
1def1ec
Update plugins/modules/cloud/misc/terraform.py
kosalaat Oct 1, 2022
980faa1
Update plugins/modules/cloud/misc/terraform.py
kosalaat Oct 1, 2022
738597b
Update plugins/modules/cloud/misc/terraform.py
kosalaat Oct 1, 2022
53c7887
Update plugins/modules/cloud/misc/terraform.py
kosalaat Oct 1, 2022
9f3063a
Update plugins/modules/cloud/misc/terraform.py
kosalaat Oct 3, 2022
ab0b1ab
Update plugins/modules/cloud/misc/terraform.py
kosalaat Oct 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/4797-terraform-complex-variables.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- terrform - adds capability to handle complex variable structures for ``variables`` parameter in the module (https://github.com/ansible-collections/community.general/pull/4797).
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
114 changes: 105 additions & 9 deletions plugins/modules/cloud/misc/terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@
aliases: [ 'variables_file' ]
variables:
description:
- A group of key-values to override template variables or those in
variables files.
- A group of key-values to override template variables or those in variables files.
- Support complex variable structures to reflect terraform variable syntax.
- Ansible dictionaries are mapped to terraform objects.
- Ansible lists are mapped to terraform lists.
- Ansible integers or floats are mapped to terraform numbers.
- Ansible booleans are mapped to terraform booleans.
- "B(Note) passwords passed as variables will be visible in the log output. Make sure to use C(no_log: true) in production!"
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
type: dict
targets:
description:
Expand Down Expand Up @@ -188,6 +193,25 @@
- /path/to/plugins_dir_1
- /path/to/plugins_dir_2

- name: Complex variables example
community.general.terraform:
project_path: '{{ project_dir }}'
state: present
variables:
vm_name: "{{ inventory_hostname }}"
vm_vcpus: 2
vm_mem: 2048
vm_additional_disks:
- label: "Third Disk"
size: 40
thin_provisioned: true
unit_number: 2
- label: "Fourth Disk"
size: 22
thin_provisioned: true
unit_number: 3
force_init: true

### Example directory structure for plugin_paths example
# $ tree /path/to/plugins_dir_1
# /path/to/plugins_dir_1/
Expand Down Expand Up @@ -237,6 +261,7 @@
import json
import tempfile
from ansible.module_utils.six.moves import shlex_quote
from ansible.module_utils.six import integer_types

from ansible.module_utils.basic import AnsibleModule

Expand Down Expand Up @@ -298,7 +323,7 @@ def get_workspace_context(bin_path, project_path):
command = [bin_path, 'workspace', 'list', '-no-color']
rc, out, err = module.run_command(command, cwd=project_path)
if rc != 0:
module.warn("Failed to list Terraform workspaces:\r\n{0}".format(err))
module.warn("Failed to list Terraform workspaces:\n{0}".format(err))
for item in out.split('\n'):
stripped_item = item.strip()
if not stripped_item:
Expand Down Expand Up @@ -360,12 +385,25 @@ def build_plan(command, project_path, variables_args, state_file, targets, state
return plan_path, False, out, err, plan_command if state == 'planned' else command
elif rc == 1:
# failure to plan
module.fail_json(msg='Terraform plan could not be created\r\nSTDOUT: {0}\r\n\r\nSTDERR: {1}'.format(out, err))
module.fail_json(
msg='Terraform plan could not be created\nSTDOUT: {out}\nSTDERR: {err}\nCOMMAND: {cmd} {args}'.format(
out=out,
err=err,
cmd=' '.join(plan_command),
args=' '.join([shlex_quote(arg) for arg in variables_args])
)
)
elif rc == 2:
# changes, but successful
return plan_path, True, out, err, plan_command if state == 'planned' else command

module.fail_json(msg='Terraform plan failed with unexpected exit code {0}. \r\nSTDOUT: {1}\r\n\r\nSTDERR: {2}'.format(rc, out, err))
module.fail_json(msg='Terraform plan failed with unexpected exit code {rc}.\nSTDOUT: {out}\nSTDERR: {err}\nCOMMAND: {cmd} {args}'.format(
rc=rc,
out=out,
err=err,
cmd=' '.join(plan_command),
args=' '.join([shlex_quote(arg) for arg in variables_args])
))


def main():
Expand Down Expand Up @@ -449,12 +487,70 @@ def main():
if state == 'present' and module.params.get('parallelism') is not None:
command.append('-parallelism=%d' % module.params.get('parallelism'))

def format_args(vars):
if isinstance(vars, str):
return '"{string}"'.format(string=vars)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should better use json.dumps() here, otherwise there will be problems with properly escaping quotes and other things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although json.dumps() do encode special characters, so is the shlex_quote(). Terraform did not like the double encapsulation, although the code/commands work without fail, it did not pass the correct string from ansible to terraform. I created a test case in https://github.com/kosalaat/terrafrom-tests using terraform null_resource, which now test all the scenarios, including checking whether string are passed in intact.

Is there a way we can make this part of the repo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the above TF workfiles, the following ansible playbooks works:

---

- hosts: localhost
  gather_facts: false
  tasks:
    - community.general.terraform:
        project_path: "../../terraform/null-resource"
        state: present
        variables:
          dictionaries:
            name: kosala
            age: 44
          list_of_strings:
            - kosala
            - neloufer
            - punu
            - ari
          list_of_objects:
            - name: kosala
              age: 44
            - name: neloufer
              age: 44
            - name: punu
              age: 9
            - name: ari
              age: 1
          boolean: true
          string_type: "randomstring2&$%@"
          list_of_lists:
            - [ 1 ]
            - [ 11, 12, 13 ]
            - [ 2 ]
            - [ 3 ]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although json.dumps() do encode special characters, so is the shlex_quote(). Terraform did not like the double encapsulation,

I'm not sure what you mean. There is no shlex_quote() involved anywhere (with the variables you are touching).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if string happens to be a"b here? Then terraform will get passed "a"b". I'm pretty sure it will not be happy with that.

Copy link
Contributor Author

@kosalaat kosalaat Aug 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not. shlex_quote() inside run_command() will sort that out. More explanation in the next comment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still wondering how strings containing " will be handled. Simply adding quotes around them is probably not ok?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to look at https://www.terraform.io/language/expressions/strings#escape-sequences to see how to correctly quote strings.

elif isinstance(vars, bool):
if vars:
return 'true'
else:
return 'false'
return str(vars)

def process_complex_args(vars):
ret_out = []
if isinstance(vars, dict):
for k, v in vars.items():
if isinstance(v, dict):
ret_out.append('{0}={{{1}}}'.format(k, process_complex_args(v)))
elif isinstance(v, list):
ret_out.append("{0}={1}".format(k, process_complex_args(v)))
elif isinstance(v, (integer_types, float, str, bool)):
ret_out.append('{0}={1}'.format(k, format_args(v)))
else:
# only to handle anything unforeseen
module.fail_json(msg="Supported types are, dictionaries, lists, strings, integer_types, boolean and float.")
if isinstance(vars, list):
l_out = []
for item in vars:
if isinstance(item, dict):
l_out.append("{{{0}}}".format(process_complex_args(item)))
elif isinstance(item, list):
l_out.append("{0}".format(process_complex_args(item)))
elif isinstance(item, (str, integer_types, float, bool)):
l_out.append(format_args(item))
else:
# only to handle anything unforeseen
module.fail_json(msg="Supported types are, dictionaries, lists, strings, integer_types, boolean and float.")

ret_out.append("[{0}]".format(",".join(l_out)))
return ",".join(ret_out)
kosalaat marked this conversation as resolved.
Show resolved Hide resolved

variables_args = []
for k, v in variables.items():
variables_args.extend([
'-var',
'{0}={1}'.format(k, v)
])
if isinstance(v, dict):
variables_args.extend([
'-var',
'{0}={{{1}}}'.format(k, process_complex_args(v))
])
elif isinstance(v, list):
variables_args.extend([
'-var',
'{0}={1}'.format(k, process_complex_args(v))
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
])
# terraform does not like shlex_quote fixing double-quotes on the top-level
# just passing the plain string and shlex_quote will take care of the rest
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
elif isinstance(v, str):
variables_args.extend([
'-var',
"{0}={1}".format(k, v)
])
else:
variables_args.extend([
'-var',
'{0}={1}'.format(k, format_args(v))
])

if variables_files:
for f in variables_files:
variables_args.extend(['-var-file', f])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

resource "null_resource" "mynullresource" {
triggers = {
# plain dictionaries
dict_name = var.dictionaries.name
dict_age = var.dictionaries.age

# list of dicrs
join_dic_name = join(",", var.list_of_objects.*.name)

# list-of-strings
join_list = join(",", var.list_of_strings.*)

# testing boolean
name = var.boolean ? var.dictionaries.name : var.list_of_objects[0].name

# top level string
sample_string = var.string_type

# nested lists
num_from_matrix = var.list_of_lists[1][2]
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

variable "dictionaries" {
type = object({
name = string
age = number
})
description = "Same as ansible Dict"
default = {
age = 1
name = "value"
}
}

variable "list_of_strings" {
type = list(string)
description = "list of strings"
}

variable "list_of_objects" {
type = list(object({
name = string
age = number
}))

}

variable "boolean" {
type = bool
description = "boolean"

}

variable "string_type" {
type = string
validation {
condition = (var.string_type == "randomstring2\"&$%@")
error_message = "Strings do not match."
}
default = "randomstring2\"&$%@"
}

variable "list_of_lists" {
type = list(list(any))
default = [ [ 1 ], [1, 2, 3], [3] ]
}
56 changes: 56 additions & 0 deletions tests/integration/targets/terraform/tasks/complex_variables.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

- name: Create terraform project directory (complex variables)
file:
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
path: "{{ terraform_project_dir }}/complex_vars"
state: directory
mode: 0755

- name: copy terraform files to work space
ansible.builtin.copy:
src: "complex_variables/{{ item }}"
dest: "{{ terraform_project_dir }}/complex_vars/{{ item }}"
with_items:
- main.tf
- variables.tf

# This task would test the various complex variable structures of the with the
# terraform null_resource
- name: test complex variables
community.general.terraform:
project_path: "{{ terraform_project_dir }}/complex_vars"
binary_path: "{{ terraform_binary_path }}"
force_init: yes
variables:
dictionaries:
name: kosala
age: 99
list_of_strings:
- kosala
- yyy
- xxx
- zzz
list_of_objects:
- name: kosala
age: 99
- name: yyy
age: 0.1
- name: zzz
age: 9.789
- name: lll
age: 1000
boolean: true
string_type: 'randomstring2"&$%@'
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
list_of_lists:
- [ 1 ]
- [ 11, 12, 13 ]
- [ 2 ]
- [ 3 ]
state: present
register: terraform_init_result

- assert:
that: terraform_init_result is not failed
12 changes: 3 additions & 9 deletions tests/integration/targets/terraform/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,13 @@
set_fact:
terraform_binary_path: "{{ terraform_binary_path.stdout or remote_tmp_dir ~ '/terraform' }}"

- name: Create terraform project directory
file:
path: "{{ terraform_project_dir }}/{{ item['name'] }}"
state: directory
mode: 0755
loop: "{{ terraform_provider_versions }}"
loop_control:
index_var: provider_index

- name: Loop over provider upgrade test tasks
include_tasks: test_provider_upgrade.yml
vars:
tf_provider: "{{ terraform_provider_versions[provider_index] }}"
loop: "{{ terraform_provider_versions }}"
loop_control:
index_var: provider_index

- name: Test Complex Varibles
include_tasks: complex_variables.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

- name: Create terraform project directory (provider upgrade)
file:
path: "{{ terraform_project_dir }}/{{ item['name'] }}"
state: directory
mode: 0755
loop: "{{ terraform_provider_versions }}"
loop_control:
index_var: provider_index

- name: Output terraform provider test project
ansible.builtin.template:
src: templates/provider_test/main.tf.j2
Expand Down