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 all 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
3 changes: 3 additions & 0 deletions changelogs/fragments/4797-terraform-complex-variables.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- terraform - adds capability to handle complex variable structures for ``variables`` parameter in the module.
This must be enabled with the new ``complex_vars`` parameter (https://github.com/ansible-collections/community.general/pull/4797).
137 changes: 127 additions & 10 deletions plugins/modules/cloud/misc/terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,25 @@
aliases: [ 'variables_file' ]
variables:
description:
- A group of key-values to override template variables or those in
variables files.
- A group of key-values pairs to override template variables or those in variables files.
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
By default, only string and number values are allowed, which are passed on unquoted.
- Support complex variable structures (lists, dictionaries, numbers, and booleans) to reflect terraform variable syntax when I(complex_vars=true).
- Ansible integers or floats are mapped to terraform numbers.
- Ansible strings are mapped to terraform strings.
- Ansible dictionaries are mapped to terraform objects.
- Ansible lists are mapped to terraform lists.
- Ansible booleans are mapped to terraform booleans.
- "B(Note) passwords passed as variables will be visible in the log output. Make sure to use I(no_log=true) in production!"
type: dict
complex_vars:
description:
- Enable/disable capability to handle complex variable structures for C(terraform).
- If C(true) the I(variables) also accepts dictionaries, lists, and booleans to be passed to C(terraform).
Strings that are passed are correctly quoted.
- When disabled, supports only simple variables (strings, integers, and floats), and passes them on unquoted.
type: bool
default: false
kosalaat marked this conversation as resolved.
Show resolved Hide resolved
version_added: 5.7.0
targets:
description:
- A list of specific resources to target in this plan/application. The
Expand Down Expand Up @@ -188,6 +204,26 @@
- /path/to/plugins_dir_1
- /path/to/plugins_dir_2

- name: Complex variables example
community.general.terraform:
project_path: '{{ project_dir }}'
state: present
camplex_vars: true
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 +273,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 +335,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 +397,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 All @@ -379,6 +429,7 @@ def main():
purge_workspace=dict(type='bool', default=False),
state=dict(default='present', choices=['present', 'absent', 'planned']),
variables=dict(type='dict'),
complex_vars=dict(type='bool', default=False),
variables_files=dict(aliases=['variables_file'], type='list', elements='path'),
plan_file=dict(type='path'),
state_file=dict(type='path'),
Expand All @@ -405,6 +456,7 @@ def main():
purge_workspace = module.params.get('purge_workspace')
state = module.params.get('state')
variables = module.params.get('variables') or {}
complex_vars = module.params.get('complex_vars')
variables_files = module.params.get('variables_files')
plan_file = module.params.get('plan_file')
state_file = module.params.get('state_file')
Expand Down Expand Up @@ -449,12 +501,77 @@ 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.replace('\\', '\\\\').replace('"', '\\"'))
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 complex_vars:
for k, v in variables.items():
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))
])
# on the top-level we need to pass just the python string with necessary
# terraform string escape sequences
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))
])
else:
for k, v in variables.items():
variables_args.extend([
'-var',
'{0}={1}'.format(k, v)
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
])

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,35 @@
# 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_1 = var.string_type

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

}

output "string_type" {
value = var.string_type
}

output "multiline_string" {
value = var.multiline_string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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"
validation {
condition = (var.list_of_strings[1] == "cli specials\"&$%@#*!(){}[]:\"\" \\\\")
error_message = "Strings do not match."
}
}

variable "list_of_objects" {
type = list(object({
name = string
age = number
}))
validation {
condition = (var.list_of_objects[1].name == "cli specials\"&$%@#*!(){}[]:\"\" \\\\")
error_message = "Strings do not match."
}
}

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

}

variable "string_type" {
type = string
validation {
condition = (var.string_type == "cli specials\"&$%@#*!(){}[]:\"\" \\\\")
error_message = "Strings do not match."
}
}

variable "multiline_string" {
type = string
validation {
condition = (var.multiline_string == "one\ntwo\n")
error_message = "Strings do not match."
}
}

variable "list_of_lists" {
type = list(list(any))
default = [ [ 1 ], [1, 2, 3], [3] ]
}
60 changes: 60 additions & 0 deletions tests/integration/targets/terraform/tasks/complex_variables.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
# 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)
ansible.builtin.file:
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
complex_vars: true
variables:
dictionaries:
name: "kosala"
age: 99
list_of_strings:
- "kosala"
- 'cli specials"&$%@#*!(){}[]:"" \\'
- "xxx"
- "zzz"
list_of_objects:
- name: "kosala"
age: 99
- name: 'cli specials"&$%@#*!(){}[]:"" \\'
age: 0.1
- name: "zzz"
age: 9.789
- name: "lll"
age: 1000
boolean: true
string_type: 'cli specials"&$%@#*!(){}[]:"" \\'
multiline_string: |
one
two
list_of_lists:
- [ 1 ]
- [ 11, 12, 13 ]
- [ 2 ]
- [ 3 ]
state: present
register: terraform_init_result

- assert:
that: terraform_init_result is not failed
Loading