From 0461620b2ddc7e8c0bf00bf8b7b95edd3a4c109c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20REY?= Date: Thu, 2 Aug 2018 15:37:26 +0200 Subject: [PATCH] Terraform: add workspace support for switching and removing workspaces (#43489) From terraform documentation: ``` The persistent data stored in the backend belongs to a workspace. Initially the backend has only one workspace, called "default", and thus there is only one Terraform state associated with that configuration. Certain backends support multiple named workspaces, allowing multiple states to be associated with a single configuration. The configuration still has only one backend, but multiple distinct instances of that configuration to be deployed without configuring a new backend or changing authentication credentials. ``` This patch introduces the `workspace` parameter in the terraform module. The module will select the workspace is it does not exists, or simply select it if it exists. Fixes #43134 Add 'purge_workspace' parameter and handle the workspace context The `purge_workspace` parameter allows to remove a workspace when asking for state = absent. It allows to leave a clean state file without empty workspaces if the parameter is true. Also adding the support of a workspace context that allows to restore the workspace when that was active when the module started. --- lib/ansible/modules/cloud/misc/terraform.py | 73 ++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/lib/ansible/modules/cloud/misc/terraform.py b/lib/ansible/modules/cloud/misc/terraform.py index bf10b84282a..5c1e8e1d146 100644 --- a/lib/ansible/modules/cloud/misc/terraform.py +++ b/lib/ansible/modules/cloud/misc/terraform.py @@ -38,6 +38,21 @@ options: - The path to the root of the Terraform directory with the vars.tf/main.tf/etc to use. required: true + workspace: + description: + - The terraform workspace to work with. + required: false + default: default + version_added: 2.7 + purge_workspace: + description: + - Only works with state = absent + - If true, the workspace will be deleted after the "terraform destroy" action. + - The 'default' workspace will not be deleted. + required: false + default: false + type: bool + version_added: 2.7 plan_file: description: - The path to an existing Terraform plan file to apply. If this is not @@ -166,6 +181,43 @@ def init_plugins(bin_path, project_path): module.fail_json(msg="Failed to initialize Terraform modules:\r\n{0}".format(err)) +def get_workspace_context(bin_path, project_path): + workspace_ctx = {"current": "default", "all": []} + command = [bin_path, 'workspace', 'list'] + rc, out, err = module.run_command(command, cwd=project_path) + if rc != 0: + module.fail_json(msg="Failed to list Terraform workspaces:\r\n{0}".format(err)) + for item in out.split('\n'): + stripped_item = item.strip() + if not stripped_item: + continue + elif stripped_item.startswith('* '): + workspace_ctx["current"] = stripped_item.replace('* ', '') + else: + workspace_ctx["all"].append(stripped_item) + return workspace_ctx + + +def _workspace_cmd(bin_path, project_path, action, workspace): + command = [bin_path, 'workspace', action, workspace] + rc, out, err = module.run_command(command, cwd=project_path) + if rc != 0: + module.fail_json(msg="Failed to {0} workspace:\r\n{1}".format(action, err)) + return rc, out, err + + +def create_workspace(bin_path, project_path, workspace): + _workspace_cmd(bin_path, project_path, 'new', workspace) + + +def select_workspace(bin_path, project_path, workspace): + _workspace_cmd(bin_path, project_path, 'select', workspace) + + +def remove_workspace(bin_path, project_path, workspace): + _workspace_cmd(bin_path, project_path, 'delete', workspace) + + def build_plan(bin_path, project_path, variables_args, state_file, targets, plan_path=None): if plan_path is None: f, plan_path = tempfile.mkstemp(suffix='.tfplan') @@ -198,6 +250,8 @@ def main(): argument_spec=dict( project_path=dict(required=True, type='path'), binary_path=dict(type='path'), + workspace=dict(required=False, type='str', default='default'), + purge_workspace=dict(type='bool', default=False), state=dict(default='present', choices=['present', 'absent', 'planned']), variables=dict(type='dict'), variables_file=dict(type='path'), @@ -206,7 +260,7 @@ def main(): targets=dict(type='list', default=[]), lock=dict(type='bool', default=True), lock_timeout=dict(type='int',), - force_init=dict(type='bool', default=False) + force_init=dict(type='bool', default=False), ), required_if=[('state', 'planned', ['plan_file'])], supports_check_mode=True, @@ -214,6 +268,8 @@ def main(): project_path = module.params.get('project_path') bin_path = module.params.get('binary_path') + workspace = module.params.get('workspace') + purge_workspace = module.params.get('purge_workspace') state = module.params.get('state') variables = module.params.get('variables') or {} variables_file = module.params.get('variables_file') @@ -229,6 +285,13 @@ def main(): if force_init: init_plugins(command[0], project_path) + workspace_ctx = get_workspace_context(command[0], project_path) + if workspace_ctx["current"] != workspace: + if workspace not in workspace_ctx["all"]: + create_workspace(command[0], project_path, workspace) + else: + select_workspace(command[0], project_path, workspace) + variables_args = [] for k, v in variables.items(): variables_args.extend([ @@ -300,7 +363,13 @@ def main(): else: outputs = json.loads(outputs_text) - module.exit_json(changed=changed, state=state, outputs=outputs, stdout=out, stderr=err, command=' '.join(command)) + # Restore the Terraform workspace found when running the module + if workspace_ctx["current"] != workspace: + select_workspace(command[0], project_path, workspace_ctx["current"]) + if state == 'absent' and workspace != 'default' and purge_workspace is True: + remove_workspace(command[0], project_path, workspace) + + module.exit_json(changed=changed, state=state, workspace=workspace, outputs=outputs, stdout=out, stderr=err, command=' '.join(command)) if __name__ == '__main__':