diff --git a/changelogs/fragments/10154-terraform_no_color_paramater.yml b/changelogs/fragments/10154-terraform_no_color_paramater.yml new file mode 100644 index 0000000000..5fa270d5ed --- /dev/null +++ b/changelogs/fragments/10154-terraform_no_color_paramater.yml @@ -0,0 +1,2 @@ +minor_changes: + - terraform - adds the ``no_color`` parameter, which suppresses or allows color codes in stdout from Terraform commands (https://github.com/ansible-collections/community.general/pull/10154). diff --git a/plugins/modules/terraform.py b/plugins/modules/terraform.py index 106702ea01..dd34ba9a0e 100644 --- a/plugins/modules/terraform.py +++ b/plugins/modules/terraform.py @@ -165,6 +165,13 @@ options: - Restrict concurrent operations when Terraform applies the plan. type: int version_added: '3.8.0' + no_color: + description: + - If V(true), suppress color codes in output from Terraform commands. + - If V(false), allows Terraform to use color codes in its output. + type: bool + default: true + version_added: 11.0.0 notes: - To just run a C(terraform plan), use check mode. requirements: ["terraform"] @@ -177,6 +184,12 @@ EXAMPLES = r""" project_path: '{{ project_dir }}' state: present +- name: Deploy with color output enabled + community.general.terraform: + project_path: '{{ project_dir }}' + state: present + no_color: false + - name: Define the backend configuration at init community.general.terraform: project_path: 'project/' @@ -291,17 +304,20 @@ def get_version(bin_path): return terraform_version -def preflight_validation(bin_path, project_path, version, variables_args=None, plan_file=None): +def preflight_validation(bin_path, project_path, version, variables_args=None, plan_file=None, no_color=True): if project_path is None or '/' not in project_path: module.fail_json(msg="Path for Terraform project can not be None or ''.") if not os.path.exists(bin_path): module.fail_json(msg="Path for Terraform binary '{0}' doesn't exist on this host - check the path and try again please.".format(bin_path)) if not os.path.isdir(project_path): module.fail_json(msg="Path for Terraform project '{0}' doesn't exist on this host - check the path and try again please.".format(project_path)) + cmd = [bin_path, 'validate'] + if no_color: + cmd.append('-no-color') if LooseVersion(version) < LooseVersion('0.15.0'): - module.run_command([bin_path, 'validate', '-no-color'] + variables_args, check_rc=True, cwd=project_path) + module.run_command(cmd + variables_args, check_rc=True, cwd=project_path) else: - module.run_command([bin_path, 'validate', '-no-color'], check_rc=True, cwd=project_path) + module.run_command(cmd, check_rc=True, cwd=project_path) def _state_args(state_file): @@ -312,8 +328,10 @@ def _state_args(state_file): return ['-state', state_file] -def init_plugins(bin_path, project_path, backend_config, backend_config_files, init_reconfigure, provider_upgrade, plugin_paths, workspace): - command = [bin_path, 'init', '-input=false', '-no-color'] +def init_plugins(bin_path, project_path, backend_config, backend_config_files, init_reconfigure, provider_upgrade, plugin_paths, workspace, no_color=True): + command = [bin_path, 'init', '-input=false'] + if no_color: + command.append('-no-color') if backend_config: for key, val in backend_config.items(): command.extend([ @@ -333,9 +351,12 @@ def init_plugins(bin_path, project_path, backend_config, backend_config_files, i rc, out, err = module.run_command(command, check_rc=True, cwd=project_path, environ_update={"TF_WORKSPACE": workspace}) -def get_workspace_context(bin_path, project_path): +def get_workspace_context(bin_path, project_path, no_color=True): workspace_ctx = {"current": "default", "all": []} - command = [bin_path, 'workspace', 'list', '-no-color'] + command = [bin_path, 'workspace', 'list'] + if no_color: + command.append('-no-color') + rc, out, err = module.run_command(command, cwd=project_path) if rc != 0: module.warn("Failed to list Terraform workspaces:\n{0}".format(err)) @@ -351,25 +372,27 @@ def get_workspace_context(bin_path, project_path): return workspace_ctx -def _workspace_cmd(bin_path, project_path, action, workspace): - command = [bin_path, 'workspace', action, workspace, '-no-color'] +def _workspace_cmd(bin_path, project_path, action, workspace, no_color=True): + command = [bin_path, 'workspace', action, workspace] + if no_color: + command.append('-no-color') rc, out, err = module.run_command(command, check_rc=True, cwd=project_path) return rc, out, err -def create_workspace(bin_path, project_path, workspace): - _workspace_cmd(bin_path, project_path, 'new', workspace) +def create_workspace(bin_path, project_path, workspace, no_color=True): + _workspace_cmd(bin_path, project_path, 'new', workspace, no_color) -def select_workspace(bin_path, project_path, workspace): - _workspace_cmd(bin_path, project_path, 'select', workspace) +def select_workspace(bin_path, project_path, workspace, no_color=True): + _workspace_cmd(bin_path, project_path, 'select', workspace, no_color) -def remove_workspace(bin_path, project_path, workspace): - _workspace_cmd(bin_path, project_path, 'delete', workspace) +def remove_workspace(bin_path, project_path, workspace, no_color=True): + _workspace_cmd(bin_path, project_path, 'delete', workspace, no_color) -def build_plan(command, project_path, variables_args, state_file, targets, state, args, plan_path=None): +def build_plan(command, project_path, variables_args, state_file, targets, state, args, plan_path=None, no_color=True): if plan_path is None: f, plan_path = tempfile.mkstemp(suffix='.tfplan') @@ -391,7 +414,10 @@ def build_plan(command, project_path, variables_args, state_file, targets, state for a in args: plan_command.append(a) - plan_command.extend(['-input=false', '-no-color', '-detailed-exitcode', '-out', plan_path]) + plan_options = ['-input=false', '-detailed-exitcode', '-out', plan_path] + if no_color: + plan_options.insert(0, '-no-color') + plan_command.extend(plan_options) for t in targets: plan_command.extend(['-target', t]) @@ -495,6 +521,7 @@ def main(): check_destroy=dict(type='bool', default=False), parallelism=dict(type='int'), provider_upgrade=dict(type='bool', default=False), + no_color=dict(type='bool', default=True), ), required_if=[('state', 'planned', ['plan_file'])], supports_check_mode=True, @@ -518,6 +545,7 @@ def main(): overwrite_init = module.params.get('overwrite_init') check_destroy = module.params.get('check_destroy') provider_upgrade = module.params.get('provider_upgrade') + no_color = module.params.get('no_color') if bin_path is not None: command = [bin_path] @@ -527,22 +555,30 @@ def main(): checked_version = get_version(command[0]) if LooseVersion(checked_version) < LooseVersion('0.15.0'): - DESTROY_ARGS = ('destroy', '-no-color', '-force') - APPLY_ARGS = ('apply', '-no-color', '-input=false', '-auto-approve=true') + if no_color: + DESTROY_ARGS = ('destroy', '-no-color', '-force') + APPLY_ARGS = ('apply', '-no-color', '-input=false', '-auto-approve=true') + else: + DESTROY_ARGS = ('destroy', '-force') + APPLY_ARGS = ('apply', '-input=false', '-auto-approve=true') else: - DESTROY_ARGS = ('destroy', '-no-color', '-auto-approve') - APPLY_ARGS = ('apply', '-no-color', '-input=false', '-auto-approve') + if no_color: + DESTROY_ARGS = ('destroy', '-no-color', '-auto-approve') + APPLY_ARGS = ('apply', '-no-color', '-input=false', '-auto-approve') + else: + DESTROY_ARGS = ('destroy', '-auto-approve') + APPLY_ARGS = ('apply', '-input=false', '-auto-approve') if force_init: if overwrite_init or not os.path.isfile(os.path.join(project_path, ".terraform", "terraform.tfstate")): - init_plugins(command[0], project_path, backend_config, backend_config_files, init_reconfigure, provider_upgrade, plugin_paths, workspace) + init_plugins(command[0], project_path, backend_config, backend_config_files, init_reconfigure, provider_upgrade, plugin_paths, workspace, no_color) - workspace_ctx = get_workspace_context(command[0], project_path) + workspace_ctx = get_workspace_context(command[0], project_path, no_color) if workspace_ctx["current"] != workspace: if workspace not in workspace_ctx["all"]: - create_workspace(command[0], project_path, workspace) + create_workspace(command[0], project_path, workspace, no_color) else: - select_workspace(command[0], project_path, workspace) + select_workspace(command[0], project_path, workspace, no_color) if state == 'present': command.extend(APPLY_ARGS) @@ -627,7 +663,7 @@ def main(): for f in variables_files: variables_args.extend(['-var-file', f]) - preflight_validation(command[0], project_path, checked_version, variables_args) + preflight_validation(command[0], project_path, checked_version, variables_args, plan_file, no_color) if module.params.get('lock') is not None: if module.params.get('lock'): @@ -654,7 +690,7 @@ def main(): module.fail_json(msg='Could not find plan_file "{0}", check the path and try again.'.format(plan_file)) else: plan_file, needs_application, out, err, command = build_plan(command, project_path, variables_args, state_file, - module.params.get('targets'), state, APPLY_ARGS, plan_file) + module.params.get('targets'), state, APPLY_ARGS, plan_file, no_color) if state == 'present' and check_destroy and '- destroy' in out: module.fail_json(msg="Aborting command because it would destroy some resources. " "Consider switching the 'check_destroy' to false to suppress this error") @@ -665,13 +701,13 @@ def main(): if state == 'absent': plan_absent_args = ['-destroy'] plan_file, needs_application, out, err, command = build_plan(command, project_path, variables_args, state_file, - module.params.get('targets'), state, plan_absent_args, plan_file) + module.params.get('targets'), state, plan_absent_args, plan_file, no_color) diff_command = [command[0], 'show', '-json', plan_file] rc, diff_output, err = module.run_command(diff_command, check_rc=False, cwd=project_path) changed, result_diff = get_diff(diff_output) if rc != 0: if workspace_ctx["current"] != workspace: - select_workspace(command[0], project_path, workspace_ctx["current"]) + select_workspace(command[0], project_path, workspace_ctx["current"], no_color) module.fail_json(msg=err.rstrip(), rc=rc, stdout=out, stdout_lines=out.splitlines(), stderr=err, stderr_lines=err.splitlines(), @@ -681,7 +717,7 @@ def main(): rc, out, err = module.run_command(command, check_rc=False, cwd=project_path) if rc != 0: if workspace_ctx["current"] != workspace: - select_workspace(command[0], project_path, workspace_ctx["current"]) + select_workspace(command[0], project_path, workspace_ctx["current"], no_color) module.fail_json(msg=err.rstrip(), rc=rc, stdout=out, stdout_lines=out.splitlines(), stderr=err, stderr_lines=err.splitlines(), @@ -690,7 +726,11 @@ def main(): if ' 0 added, 0 changed' not in out and not state == "absent" or ' 0 destroyed' not in out: changed = True - outputs_command = [command[0], 'output', '-no-color', '-json'] + _state_args(state_file) + if no_color: + outputs_command = [command[0], 'output', '-no-color', '-json'] + _state_args(state_file) + else: + outputs_command = [command[0], 'output', '-json'] + _state_args(state_file) + rc, outputs_text, outputs_err = module.run_command(outputs_command, cwd=project_path) outputs = {} if rc == 1: @@ -705,9 +745,9 @@ def main(): # Restore the Terraform workspace found when running the module if workspace_ctx["current"] != workspace: - select_workspace(command[0], project_path, workspace_ctx["current"]) + select_workspace(command[0], project_path, workspace_ctx["current"], no_color) if state == 'absent' and workspace != 'default' and purge_workspace is True: - remove_workspace(command[0], project_path, workspace) + remove_workspace(command[0], project_path, workspace, no_color) result = { 'state': state,