Repository: ansible-collections/community.general

Issue:
docker_login removes any other existing logins from .docker/config.json
_From @dwagelaar on Oct 16, 2020 15:00_

<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Explain the problem briefly below -->
I'm using two docker_login tasks to log into two different repositories. Only the last logged in repository remains in .docker/config.json

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
docker_login

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible 2.9.13
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/hcadmin/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.6.8 (default, Apr 16 2020, 01:36:27) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
ANSIBLE_PIPELINING(/etc/ansible/ansible.cfg) = True
DEFAULT_FORKS(/etc/ansible/ansible.cfg) = 100
DEFAULT_PRIVATE_KEY_FILE(/etc/ansible/ansible.cfg) = /home/hcadmin/.ssh/id_rsa_ansible
DEFAULT_ROLES_PATH(/etc/ansible/ansible.cfg) = ['/etc/ansible/roles', '/usr/share/ansible/roles', '/home/hcadmin/ansible/roles']
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
NAME="Flatcar Container Linux by Kinvolk"
ID=flatcar
ID_LIKE=coreos
VERSION=2605.6.0
VERSION_ID=2605.6.0
BUILD_ID=2020-09-28-2140
PRETTY_NAME="Flatcar Container Linux by Kinvolk 2605.6.0 (Oklo)"
ANSI_COLOR="38;5;75"
HOME_URL="https://flatcar-linux.org/"
BUG_REPORT_URL="https://issues.flatcar-linux.org"
FLATCAR_BOARD="amd64-usr"

Python 2.7.13 (d0d41085a83c, Apr 06 2020, 17:09:17)
[PyPy 7.3.1 with GCC 7.3.1 20180303 (Red Hat 7.3.1-5)]

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
Use community.general.docker_login to log into two docker registries.

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Log into quay.io
  community.general.docker_login:
    registry_url: quay.io
    username: "corilus+hcdeploy"
    password: "{{ docker_login_quay_io_password }}"

- name: Log into harbor.corilus.be (careconnect_sf)
  community.general.docker_login:
    registry_url: harbor.corilus.be
    username: "robot$deploy"
    password: "{{ docker_login_harbor_careconnect_sf_password }}"
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
Both registries should appear in .docker/config.json, just as with the "docker login" command.

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
Only the last executed login remains in .docker/config.json.

<!--- Paste verbatim command output between quotes -->
```paste below
$ ansible-playbook -i ansible/inventories/qa/ --vault-id @prompt ansible/docker-login.yml -l cc-dock
Vault password (default):
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details

PLAY [Log in docker nodes to docker registries] *******************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************************************
ok: [docker04.qa.careconnect.be]
ok: [docker03.qa.careconnect.be]
ok: [docker01.qa.careconnect.be]
ok: [docker02.qa.careconnect.be]

TASK [docker_login : Install the community.general Ansible collection locally] ************************************************************************************************************************
ok: [docker01.qa.careconnect.be]

TASK [docker_login : Execute the docker login] ********************************************************************************************************************************************************
included: /home/dennis.wagelaar/ansible/roles/docker_login/tasks/login.yml for docker01.qa.careconnect.be, docker02.qa.careconnect.be, docker03.qa.careconnect.be, docker04.qa.careconnect.be

TASK [docker_login : Log into quay.io] ****************************************************************************************************************************************************************
changed: [docker03.qa.careconnect.be]
changed: [docker04.qa.careconnect.be]
changed: [docker01.qa.careconnect.be]
changed: [docker02.qa.careconnect.be]

TASK [docker_login : Log into harbor.corilus.be (careconnect_sf)] *************************************************************************************************************************************
changed: [docker01.qa.careconnect.be]
changed: [docker02.qa.careconnect.be]
changed: [docker03.qa.careconnect.be]
changed: [docker04.qa.careconnect.be]

PLAY RECAP ********************************************************************************************************************************************************************************************
docker01.qa.careconnect.be : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
docker02.qa.careconnect.be : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
docker03.qa.careconnect.be : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
docker04.qa.careconnect.be : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```


_Copied from original issue: ansible/ansible#72236_


Discussion:
Files identified in the description:
None

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 --->
resolved_by_pr #1118.
Files identified in the description:
* [`plugins/modules/cloud/docker/docker_login.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_login.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 @DBendit @WojciechowskiPiotr @akshay196 @chouseknecht @danihodovic @dariko @jwitko @kassiansun @olsaki @tbouvet
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/cloud/docker/docker_login.py b/plugins/modules/cloud/docker/docker_login.py
--- a/plugins/modules/cloud/docker/docker_login.py
+++ b/plugins/modules/cloud/docker/docker_login.py
@@ -257,14 +257,13 @@ def store(self, server, username, password):
         auth = to_text(b64auth)
 
         # build up the auth structure
-        new_auth = dict(
-            auths=dict()
-        )
-        new_auth['auths'][server] = dict(
+        if 'auths' not in self._config:
+            self._config['auths'] = dict()
+
+        self._config['auths'][server] = dict(
             auth=auth
         )
 
-        self._config.update(new_auth)
         self._write()
 
     def erase(self, server):
@@ -272,8 +271,9 @@ def erase(self, server):
         Remove credentials for the given server from the configuration.
         '''
 
-        self._config['auths'].pop(server)
-        self._write()
+        if 'auths' in self._config and server in self._config['auths']:
+            self._config['auths'].pop(server)
+            self._write()
 
 
 class LoginManager(DockerBaseClass):


Test patch:
diff --git a/tests/integration/targets/docker_login/tasks/main.yml b/tests/integration/targets/docker_login/tasks/main.yml
--- a/tests/integration/targets/docker_login/tasks/main.yml
+++ b/tests/integration/targets/docker_login/tasks/main.yml
@@ -1,3 +1,4 @@
+---
 ####################################################################
 # WARNING: These are designed specifically for Ansible tests       #
 # and should not be used as examples of how to write Ansible roles #
diff --git a/tests/integration/targets/docker_login/tasks/test.yml b/tests/integration/targets/docker_login/tasks/test.yml
--- a/tests/integration/targets/docker_login/tasks/test.yml
+++ b/tests/integration/targets/docker_login/tasks/test.yml
@@ -1,3 +1,4 @@
+---
 - block:
   - include_tasks: run-test.yml
     with_fileglob:
diff --git a/tests/integration/targets/setup_docker_registry/handlers/cleanup.yml b/tests/integration/targets/setup_docker_registry/handlers/cleanup.yml
--- a/tests/integration/targets/setup_docker_registry/handlers/cleanup.yml
+++ b/tests/integration/targets/setup_docker_registry/handlers/cleanup.yml
@@ -1,10 +1,11 @@
+---
 - name: "Make sure all images are removed"
   docker_image:
     name: "{{ item }}"
     state: absent
-  with_items: "{{ inames }}"
+  with_items: "{{ docker_registry_setup_inames }}"
 - name: "Get registry logs"
-  command: "docker logs {{ registry_name }}"
+  command: "docker logs {{ docker_registry_container_name_registry }}"
   register: registry_logs
   no_log: yes
   ignore_errors: yes
@@ -12,26 +13,35 @@
   debug:
     var: registry_logs.stdout_lines
   when: registry_logs is not failed
-- name: "Get nginx logs"
-  command: "docker logs {{ nginx_name }}"
+- name: "Get nginx logs for first instance"
+  command: "docker logs {{ docker_registry_container_name_nginx }}"
   register: nginx_logs
   no_log: yes
   ignore_errors: yes
-- name: "Printing nginx logs"
+- name: "Get nginx logs for second instance"
+  command: "docker logs {{ docker_registry_container_name_nginx2 }}"
+  register: nginx2_logs
+  no_log: yes
+  ignore_errors: yes
+- name: "Printing nginx logs for first instance"
   debug:
     var: nginx_logs.stdout_lines
   when: nginx_logs is not failed
+- name: "Printing nginx logs for second instance"
+  debug:
+    var: nginx2_logs.stdout_lines
+  when: nginx_logs is not failed
 - name: "Make sure all containers are removed"
   docker_container:
     name: "{{ item }}"
     state: absent
     force_kill: yes
-  with_items: "{{ cnames }}"
+  with_items: "{{ docker_registry_setup_cnames }}"
   register: result
   retries: 3
   delay: 3
   until: result is success
 - name: "Make sure all volumes are removed"
   command: "docker rm -f {{ item }}"
-  with_items: "{{ vnames }}"
+  with_items: "{{ docker_registry_setup_vnames }}"
   ignore_errors: yes
diff --git a/tests/integration/targets/setup_docker_registry/handlers/main.yml b/tests/integration/targets/setup_docker_registry/handlers/main.yml
--- a/tests/integration/targets/setup_docker_registry/handlers/main.yml
+++ b/tests/integration/targets/setup_docker_registry/handlers/main.yml
@@ -1,2 +1,3 @@
+---
 - name: Remove test registry
   include_tasks: ../handlers/cleanup.yml
diff --git a/tests/integration/targets/setup_docker_registry/meta/main.yml b/tests/integration/targets/setup_docker_registry/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/setup_docker_registry/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+  #- setup_docker  -- done in setup.yml, to work around cleanup problems!
+  - setup_openssl
diff --git a/tests/integration/targets/setup_docker_registry/tasks/main.yml b/tests/integration/targets/setup_docker_registry/tasks/main.yml
--- a/tests/integration/targets/setup_docker_registry/tasks/main.yml
+++ b/tests/integration/targets/setup_docker_registry/tasks/main.yml
@@ -1,3 +1,4 @@
+---
 ####################################################################
 # WARNING: These are designed specifically for Ansible tests       #
 # and should not be used as examples of how to write Ansible roles #
diff --git a/tests/integration/targets/setup_docker_registry/tasks/setup.yml b/tests/integration/targets/setup_docker_registry/tasks/setup.yml
--- a/tests/integration/targets/setup_docker_registry/tasks/setup.yml
+++ b/tests/integration/targets/setup_docker_registry/tasks/setup.yml
@@ -1,54 +1,60 @@
-- name: Setup OpenSSL
-  include_role:
-    name: setup_openssl
+---
 - name: Register registry cleanup
   command: 'true'
   notify: Remove test registry
 - name: Setup Docker
+  # Please note that we do setup_docker here and not via meta/main.yml to avoid the problem that
+  # our cleanup is called **after** setup_docker's cleanup has been called!
   include_role:
     name: setup_docker
 - name: Create random name prefix and test registry name
   set_fact:
-    name_prefix: '{{ ''ansible-test-%0x'' % ((2**32) | random) }}'
-    registry_name: '{{ ''ansible-test-registry-%0x'' % ((2**32) | random) }}'
-    nginx_name: '{{ ''ansible-test-registry-frontend-%0x'' % ((2**32) | random) }}'
+    docker_registry_container_name_registry: '{{ ''ansible-test-registry-%0x'' % ((2**32) | random) }}'
+    docker_registry_container_name_nginx: '{{ ''ansible-test-registry-frontend-%0x'' % ((2**32) | random) }}'
+    docker_registry_container_name_nginx2: '{{ ''ansible-test-registry-frontend2-%0x'' % ((2**32) | random) }}'
 - name: Create image and container list
   set_fact:
-    inames: []
-    cnames:
-    - '{{ registry_name }}'
-    - '{{ nginx_name }}'
-    vnames:
-    - '{{ nginx_name }}'
+    docker_registry_setup_inames: []
+    docker_registry_setup_cnames:
+    - '{{ docker_registry_container_name_registry }}'
+    - '{{ docker_registry_container_name_nginx }}'
+    - '{{ docker_registry_container_name_nginx2 }}'
+    docker_registry_setup_vnames:
+    - '{{ docker_registry_container_name_nginx }}'
+    - '{{ docker_registry_container_name_nginx2 }}'
 - debug:
-    msg: Using name prefix {{ name_prefix }} and test registry name {{ registry_name }}
+    msg: Using test registry name {{ docker_registry_container_name_registry }} and nginx frontend name {{ docker_registry_container_name_nginx }}
 - block:
+
+  # Set up registry container
   - name: Start test registry
     docker_container:
-      name: '{{ registry_name }}'
+      name: '{{ docker_registry_container_name_registry }}'
       image: registry:2.6.1
       ports: 5000
     register: registry_container
   - name: Get registry URL
     set_fact:
       registry_address: localhost:{{ registry_container.container.NetworkSettings.Ports['5000/tcp'].0.HostPort }}
+
+  # Set up first nginx frontend for registry
   - name: Start nginx frontend for registry
     docker_volume:
-      name: '{{ nginx_name }}'
+      name: '{{ docker_registry_container_name_nginx }}'
       state: present
   - name: Create container for nginx frontend for registry
     docker_container:
       state: stopped
-      name: '{{ nginx_name }}'
+      name: '{{ docker_registry_container_name_nginx }}'
       image: nginx:alpine
       ports: 5000
       links:
-      - '{{ registry_name }}:real-registry'
+      - '{{ docker_registry_container_name_registry }}:real-registry'
       volumes:
-      - '{{ nginx_name }}:/etc/nginx/'
+      - '{{ docker_registry_container_name_nginx }}:/etc/nginx/'
     register: nginx_container
   - name: Copy static files into volume
-    command: docker cp {{ role_path }}/files/{{ item }} {{ nginx_name }}:/etc/nginx/{{ item }}
+    command: docker cp {{ role_path }}/files/{{ item }} {{ docker_registry_container_name_nginx }}:/etc/nginx/{{ item }}
     loop:
     - nginx.conf
     - nginx.htpasswd
@@ -73,13 +79,13 @@
         privatekey_path: '{{ output_dir }}/cert.key'
         provider: selfsigned
     - name: Copy dynamic files into volume
-      command: docker cp {{ output_dir }}/{{ item }} {{ nginx_name }}:/etc/nginx/{{ item }}
+      command: docker cp {{ output_dir }}/{{ item }} {{ docker_registry_container_name_nginx }}:/etc/nginx/{{ item }}
       loop:
       - cert.pem
       - cert.key
     - name: Start nginx frontend for registry
       docker_container:
-        name: '{{ nginx_name }}'
+        name: '{{ docker_registry_container_name_nginx }}'
         state: started
       register: nginx_container
     - debug: var=nginx_container.container.NetworkSettings
@@ -100,7 +106,78 @@
   - set_fact:
       registry_frontend_address: 'n/a'
     when: can_copy_files is failed
-  - debug: msg="Registry available under {{ registry_address }}, NGINX frontend available under {{ registry_frontend_address }}"
+
+  # Set up second nginx frontend for registry
+  - name: Start nginx frontend for registry
+    docker_volume:
+      name: '{{ docker_registry_container_name_nginx2 }}'
+      state: present
+  - name: Create container for nginx frontend for registry
+    docker_container:
+      state: stopped
+      name: '{{ docker_registry_container_name_nginx2 }}'
+      image: nginx:alpine
+      ports: 5000
+      links:
+      - '{{ docker_registry_container_name_registry }}:real-registry'
+      volumes:
+      - '{{ docker_registry_container_name_nginx2 }}:/etc/nginx/'
+    register: nginx_container
+  - name: Copy static files into volume
+    command: docker cp {{ role_path }}/files/{{ item }} {{ docker_registry_container_name_nginx2 }}:/etc/nginx/{{ item }}
+    loop:
+    - nginx.conf
+    - nginx.htpasswd
+    register: can_copy_files
+    ignore_errors: yes
+  - block:
+    - name: Create private key for frontend certificate
+      community.crypto.openssl_privatekey:
+        path: '{{ output_dir }}/cert.key'
+        type: ECC
+        curve: secp256r1
+    - name: Create CSR for frontend certificate
+      community.crypto.openssl_csr:
+        path: '{{ output_dir }}/cert.csr'
+        privatekey_path: '{{ output_dir }}/cert.key'
+        subject_alt_name:
+        - DNS:test-registry.ansible.com
+    - name: Create frontend certificate
+      community.crypto.openssl_certificate:
+        path: '{{ output_dir }}/cert.pem'
+        csr_path: '{{ output_dir }}/cert.csr'
+        privatekey_path: '{{ output_dir }}/cert.key'
+        provider: selfsigned
+    - name: Copy dynamic files into volume
+      command: docker cp {{ output_dir }}/{{ item }} {{ docker_registry_container_name_nginx2 }}:/etc/nginx/{{ item }}
+      loop:
+      - cert.pem
+      - cert.key
+    - name: Start nginx frontend for registry
+      docker_container:
+        name: '{{ docker_registry_container_name_nginx2 }}'
+        state: started
+      register: nginx_container
+    - debug: var=nginx_container.container.NetworkSettings
+    - name: Wait for registry frontend
+      uri:
+        url: https://{{ nginx_container.container.NetworkSettings.IPAddress }}:5000/v2/
+        url_username: testuser
+        url_password: hunter2
+        validate_certs: false
+      register: result
+      until: result is success
+      retries: 5
+      delay: 1
+    - name: Get registry URL
+      set_fact:
+        registry_frontend2_address: localhost:{{ nginx_container.container.NetworkSettings.Ports['5000/tcp'].0.HostPort }}
+    when: can_copy_files is not failed
+  - set_fact:
+      registry_frontend2_address: 'n/a'
+    when: can_copy_files is failed
+
+  - debug: msg="Registry available under {{ registry_address }}, NGINX frontends available under {{ registry_frontend_address }} and {{ registry_frontend2_address }}"
   when: docker_py_version is version('1.8.0', '>=') and docker_api_version is version('1.20', '>=')
 - fail: msg="Too old docker / docker-py version to run docker_image tests!"
   when: not(docker_py_version is version('1.8.0', '>=') and docker_api_version is version('1.20', '>=')) and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)