Repository: ansible/ansible

Issue:
k8s module ignores namespace parameter when creating template
<!---
Verify first that your issue/request is not already reported on GitHub.
THIS FORM WILL BE READ BY A MACHINE, COMPLETE ALL SECTIONS AS DESCRIBED.
Also test if the latest release, and devel branch are affected too.
ALWAYS add information AFTER (OUTSIDE) these html comments.
Otherwise it may end up being automatically closed by our bot. -->

##### SUMMARY
<!--- Explain the problem briefly -->

The namespace parameter in the k8s module is ignored when trying to create a template in OpenShift.

This is mostly an issue when the resource definition is in a separate file, as the namespace has to be manually changed in the template config for each deployment.

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->

k8s

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
ansible 2.6.0                                                                                                        
  config file = /etc/ansible/ansible.cfg                                                                             
  configured module search path = [u'/home/frestr01/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/dist-packages/ansible                                    
  executable location = /usr/local/bin/ansible                                                                       
  python version = 2.7.12 (default, Dec  4 2017, 14:50:18) [GCC 5.4.0 20160609]                                      
```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->

DEFAULT_STDOUT_CALLBACK(env: ANSIBLE_STDOUT_CALLBACK) = debug

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.-->

Ubuntu 16.04
OpenShift 3.9.0

##### STEPS TO REPRODUCE
<!--- For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used. -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
- name: "Create template"
  hosts: localhost

  tasks:
    - k8s:
        state: present
        namespace: test-ns
        definition:
          apiVersion: v1
          kind: Template
          metadata:
            name: test
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

The test template is created in the test-ns namespace.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

The error `ValueError: Namespace is required for v1.Template` is generated.

The result is the same if using the resource_definition parameter instead of definition in the playbook.

If the namespace parameter is inside the definition, the template creation succeeds. Thus, it seems like k8s doesn't add the specified namespace to the template.


<!--- Paste verbatim command output between quotes below -->
```
ansible-playbook 2.6.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/frestr01/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/dist-packages/ansible
  executable location = /usr/local/bin/ansible-playbook
  python version = 2.7.12 (default, Dec  4 2017, 14:50:18) [GCC 5.4.0 20160609]
Using /etc/ansible/ansible.cfg as config file
Parsed /etc/ansible/hosts inventory source with ini plugin
 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match
'all'


PLAYBOOK: test.yml ****************************************************************************************************
1 plays in test.yml

PLAY [Create template] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
task path: /home/frestr01/Documents/ans_k8s_bug_reproduce/test.yml:2
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: frestr01
<127.0.0.1> EXEC /bin/sh -c 'echo ~frestr01 && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/frestr01/.ansible/tmp/ansible-tmp-1530618157.54-77764
151602697 `" && echo ansible-tmp-1530618157.54-77764151602697="` echo /home/frestr01/.ansible/tmp/ansible-tmp-153061815
7.54-77764151602697 `" ) && sleep 0'
Using module file /usr/local/lib/python2.7/dist-packages/ansible/modules/system/setup.py
<127.0.0.1> PUT /home/frestr01/.ansible/tmp/ansible-local-8978cVmQq7/tmpYqNggF TO /home/frestr01/.ansible/tmp/ansible-t
mp-1530618157.54-77764151602697/setup.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/frestr01/.ansible/tmp/ansible-tmp-1530618157.54-77764151602697/ /home/fres
tr01/.ansible/tmp/ansible-tmp-1530618157.54-77764151602697/setup.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /home/frestr01/.ansible/tmp/ansible-tmp-1530618157.54-77764151602697/setup
.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /home/frestr01/.ansible/tmp/ansible-tmp-1530618157.54-77764151602697/ > /dev/null
 2>&1 && sleep 0'
ok: [localhost]
META: ran handlers

TASK [k8s] ************************************************************************************************************
task path: /home/frestr01/Documents/ans_k8s_bug_reproduce/test.yml:6
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: frestr01
<127.0.0.1> EXEC /bin/sh -c 'echo ~frestr01 && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/frestr01/.ansible/tmp/ansible-tmp-1530618160.08-16423
9037550164 `" && echo ansible-tmp-1530618160.08-164239037550164="` echo /home/frestr01/.ansible/tmp/ansible-tmp-1530618
160.08-164239037550164 `" ) && sleep 0'
Using module file /usr/local/lib/python2.7/dist-packages/ansible/modules/clustering/k8s/k8s.py
<127.0.0.1> PUT /home/frestr01/.ansible/tmp/ansible-local-8978cVmQq7/tmp5VvXqz TO /home/frestr01/.ansible/tmp/ansible-t
mp-1530618160.08-164239037550164/k8s.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/frestr01/.ansible/tmp/ansible-tmp-1530618160.08-164239037550164/ /home/fre
str01/.ansible/tmp/ansible-tmp-1530618160.08-164239037550164/k8s.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /home/frestr01/.ansible/tmp/ansible-tmp-1530618160.08-164239037550164/k8s.
py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /home/frestr01/.ansible/tmp/ansible-tmp-1530618160.08-164239037550164/ > /dev/nul
l 2>&1 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_CUZWXz/ansible_module_k8s.py", line 165, in <module>
    main()
  File "/tmp/ansible_CUZWXz/ansible_module_k8s.py", line 161, in main
    KubernetesRawModule().execute_module()
  File "/tmp/ansible_CUZWXz/ansible_modlib.zip/ansible/module_utils/k8s/raw.py", line 80, in execute_module
  File "/tmp/ansible_CUZWXz/ansible_modlib.zip/ansible/module_utils/k8s/raw.py", line 138, in perform_action
  File "/usr/local/lib/python2.7/dist-packages/openshift/dynamic/client.py", line 31, in inner
    resp = func(self, resource, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/openshift/dynamic/client.py", line 159, in create
    namespace = self.ensure_namespace(resource, namespace, body)
  File "/usr/local/lib/python2.7/dist-packages/openshift/dynamic/client.py", line 142, in ensure_namespace
    raise ValueError("Namespace is required for {}.{}".format(resource.group_version, resource.kind))
ValueError: Namespace is required for v1.Template

fatal: [localhost]: FAILED! => {
    "changed": false, 
    "rc": 1
}

MSG:

MODULE FAILURE


MODULE_STDERR:

Traceback (most recent call last):
  File "/tmp/ansible_CUZWXz/ansible_module_k8s.py", line 165, in <module>
    main()
  File "/tmp/ansible_CUZWXz/ansible_module_k8s.py", line 161, in main
    KubernetesRawModule().execute_module()
  File "/tmp/ansible_CUZWXz/ansible_modlib.zip/ansible/module_utils/k8s/raw.py", line 80, in execute_module
  File "/tmp/ansible_CUZWXz/ansible_modlib.zip/ansible/module_utils/k8s/raw.py", line 138, in perform_action
  File "/usr/local/lib/python2.7/dist-packages/openshift/dynamic/client.py", line 31, in inner
    resp = func(self, resource, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/openshift/dynamic/client.py", line 159, in create
    namespace = self.ensure_namespace(resource, namespace, body)
  File "/usr/local/lib/python2.7/dist-packages/openshift/dynamic/client.py", line 142, in ensure_namespace
    raise ValueError("Namespace is required for {}.{}".format(resource.group_version, resource.kind))
ValueError: Namespace is required for v1.Template


        to retry, use: --limit @/home/frestr01/Documents/ans_k8s_bug_reproduce/test.retry

PLAY RECAP ************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1   
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/clustering/k8s/k8s.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/clustering/k8s/k8s.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 @chouseknecht @fabianvf @flaper87 @maxamillion
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/module_utils/k8s/raw.py b/lib/ansible/module_utils/k8s/raw.py
--- a/lib/ansible/module_utils/k8s/raw.py
+++ b/lib/ansible/module_utils/k8s/raw.py
@@ -46,10 +46,10 @@ def __init__(self, *args, **kwargs):
                                          supports_check_mode=True,
                                          **kwargs)
 
-        kind = self.params.pop('kind')
-        api_version = self.params.pop('api_version')
-        name = self.params.pop('name')
-        namespace = self.params.pop('namespace')
+        self.kind = self.params.pop('kind')
+        self.api_version = self.params.pop('api_version')
+        self.name = self.params.pop('name')
+        self.namespace = self.params.pop('namespace')
         resource_definition = self.params.pop('resource_definition')
         if resource_definition:
             if isinstance(resource_definition, string_types):
@@ -67,11 +67,11 @@ def __init__(self, *args, **kwargs):
 
         if not resource_definition and not src:
             self.resource_definitions = [{
-                'kind': kind,
-                'apiVersion': api_version,
+                'kind': self.kind,
+                'apiVersion': self.api_version,
                 'metadata': {
-                    'name': name,
-                    'namespace': namespace
+                    'name': self.name,
+                    'namespace': self.namespace
                 }
             }]
 
@@ -80,14 +80,13 @@ def execute_module(self):
         results = []
         self.client = self.get_api_client()
         for definition in self.resource_definitions:
-            kind = definition.get('kind')
+            kind = definition.get('kind', self.kind)
             search_kind = kind
             if kind.lower().endswith('list'):
                 search_kind = kind[:-4]
-            api_version = definition.get('apiVersion')
+            api_version = definition.get('apiVersion', self.api_version)
             resource = self.find_resource(search_kind, api_version, fail=True)
-            definition['kind'] = resource.kind
-            definition['apiVersion'] = resource.group_version
+            definition = self.set_defaults(resource, definition)
             result = self.perform_action(resource, definition)
             changed = changed or result['changed']
             results.append(result)
@@ -102,12 +101,23 @@ def execute_module(self):
             }
         })
 
+    def set_defaults(self, resource, definition):
+        definition['kind'] = resource.kind
+        definition['apiVersion'] = resource.group_version
+        if not definition.get('metadata'):
+            definition['metadata'] = {}
+        if self.name and not definition['metadata'].get('name'):
+            definition['metadata']['name'] = self.name
+        if resource.namespaced and self.namespace and not definition['metadata'].get('namespace'):
+            definition['metadata']['namespace'] = self.namespace
+        return definition
+
     def perform_action(self, resource, definition):
         result = {'changed': False, 'result': {}}
         state = self.params.get('state', None)
         force = self.params.get('force', False)
-        name = definition.get('metadata', {}).get('name')
-        namespace = definition.get('metadata', {}).get('namespace')
+        name = definition['metadata'].get('name')
+        namespace = definition['metadata'].get('namespace')
         existing = None
 
         self.remove_aliases()


Test patch: