Repository: ansible/ansible

Issue:
Add support of workspaces in terraform module
##### SUMMARY

From Terraform documentation: 
```
Certain backends support multiple named workspaces, allowing multiple states to be associated with a single configuration.
```

The module currently does not support the notion of workspace. Adding it would be helpful.
The default workspace would be "default" and would not break the current usage of the module.

##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
terraform

##### ANSIBLE VERSION

```
ansible 2.6.0
  config file = /Users/remirey/dev/grtgas/avignon/ansible.cfg
  configured module search path = [u'/Users/remirey/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/remirey/.virtualenvs/avignon/lib/python2.7/site-packages/ansible
  executable location = /Users/remirey/.virtualenvs/avignon/bin/ansible
  python version = 2.7.13 (default, Jul 18 2017, 09:17:00) [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)]
```

##### OS / ENVIRONMENT
N/A

##### STEPS TO REPRODUCE

<!--- Paste example playbooks or commands between quotes below -->
```yaml
# Basic deploy of a service with workspace
- terraform:
    project_path: '{{ project_dir }}'
    state: present
    workspace: staging
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/misc/terraform.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/misc/terraform.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @ryansb
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/misc/terraform.py b/lib/ansible/modules/cloud/misc/terraform.py
--- a/lib/ansible/modules/cloud/misc/terraform.py
+++ b/lib/ansible/modules/cloud/misc/terraform.py
@@ -38,6 +38,21 @@
       - 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__':


Test patch: