From 03320466995a91f8a4d311e66cdf3eaee3f44934 Mon Sep 17 00:00:00 2001
From: Rick Elrod <rick@elrod.me>
Date: Mon, 5 Oct 2020 20:13:52 -0500
Subject: [PATCH] [ansible-test] attempt to work around podman (#72096)

Change:
- podman > 2 && < 2.2 does not support "images --format {{json .}}"
- podman also now outputs images JSON differently than docker
- Work around both of the above.

Test Plan:
- Tested with podman 2.0.6 in Fedora 31.

Signed-off-by: Rick Elrod <rick@elrod.me>
Co-authored-by: Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua>
---
 .../ansible-test-podman-json-format.yml       |   2 +
 .../lib/ansible_test/_internal/docker_util.py |  16 ++-
 test/units/ansible_test/test_docker_util.py   | 131 ++++++++++++++++++
 3 files changed, 146 insertions(+), 3 deletions(-)
 create mode 100644 changelogs/fragments/ansible-test-podman-json-format.yml
 create mode 100644 test/units/ansible_test/test_docker_util.py

diff --git a/changelogs/fragments/ansible-test-podman-json-format.yml b/changelogs/fragments/ansible-test-podman-json-format.yml
new file mode 100644
index 00000000000..6009957f129
--- /dev/null
+++ b/changelogs/fragments/ansible-test-podman-json-format.yml
@@ -0,0 +1,2 @@
+minor_changes:
+  - ansible-test - now makes a better attempt to support podman when calling ``docker images`` and asking for JSON format.
diff --git a/test/lib/ansible_test/_internal/docker_util.py b/test/lib/ansible_test/_internal/docker_util.py
index 1807d063f67..dad07ba43b1 100644
--- a/test/lib/ansible_test/_internal/docker_util.py
+++ b/test/lib/ansible_test/_internal/docker_util.py
@@ -194,11 +194,21 @@ def docker_images(args, image):
         stdout, _dummy = docker_command(args, ['images', image, '--format', '{{json .}}'], capture=True, always=True)
     except SubprocessError as ex:
         if 'no such image' in ex.stderr:
-            stdout = ''  # podman does not handle this gracefully, exits 125
+            return []  # podman does not handle this gracefully, exits 125
+
+        if 'function "json" not defined' in ex.stderr:
+            # podman > 2 && < 2.2.0 breaks with --format {{json .}}, and requires --format json
+            # So we try this as a fallback. If it fails again, we just raise the exception and bail.
+            stdout, _dummy = docker_command(args, ['images', image, '--format', 'json'], capture=True, always=True)
         else:
             raise ex
-    results = [json.loads(line) for line in stdout.splitlines()]
-    return results
+
+    if stdout.startswith('['):
+        # modern podman outputs a pretty-printed json list. Just load the whole thing.
+        return json.loads(stdout)
+
+    # docker outputs one json object per line (jsonl)
+    return [json.loads(line) for line in stdout.splitlines()]
 
 
 def docker_rm(args, container_id):
diff --git a/test/units/ansible_test/test_docker_util.py b/test/units/ansible_test/test_docker_util.py
new file mode 100644
index 00000000000..8427f0f27db
--- /dev/null
+++ b/test/units/ansible_test/test_docker_util.py
@@ -0,0 +1,131 @@
+# This file is part of Ansible
+# -*- coding: utf-8 -*-
+#
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+import pytest
+from units.compat.mock import call, patch, MagicMock
+
+# docker images quay.io/ansible/centos7-test-container --format '{{json .}}'
+DOCKER_OUTPUT_MULTIPLE = """
+{"Containers":"N/A","CreatedAt":"2020-06-11 17:05:58 -0500 CDT","CreatedSince":"3 months ago","Digest":"\u003cnone\u003e","ID":"b0f914b26cc1","Repository":"quay.io/ansible/centos7-test-container","SharedSize":"N/A","Size":"556MB","Tag":"1.17.0","UniqueSize":"N/A","VirtualSize":"555.6MB"}
+{"Containers":"N/A","CreatedAt":"2020-06-11 17:05:58 -0500 CDT","CreatedSince":"3 months ago","Digest":"\u003cnone\u003e","ID":"b0f914b26cc1","Repository":"quay.io/ansible/centos7-test-container","SharedSize":"N/A","Size":"556MB","Tag":"latest","UniqueSize":"N/A","VirtualSize":"555.6MB"}
+{"Containers":"N/A","CreatedAt":"2019-04-01 19:59:39 -0500 CDT","CreatedSince":"18 months ago","Digest":"\u003cnone\u003e","ID":"dd3d10e03dd3","Repository":"quay.io/ansible/centos7-test-container","SharedSize":"N/A","Size":"678MB","Tag":"1.8.0","UniqueSize":"N/A","VirtualSize":"678MB"}
+""".lstrip()  # noqa: E501
+
+PODMAN_OUTPUT = """
+[
+    {
+        "id": "dd3d10e03dd3580de865560c3440c812a33fd7a1fca8ed8e4a1219ff3d809e3a",
+        "names": [
+            "quay.io/ansible/centos7-test-container:1.8.0"
+        ],
+        "digest": "sha256:6e5d9c99aa558779715a80715e5cf0c227a4b59d95e6803c148290c5d0d9d352",
+        "created": "2019-04-02T00:59:39.234584184Z",
+        "size": 702761933
+    },
+    {
+        "id": "b0f914b26cc1088ab8705413c2f2cf247306ceeea51260d64c26894190d188bd",
+        "names": [
+            "quay.io/ansible/centos7-test-container:latest"
+        ],
+        "digest": "sha256:d8431aa74f60f4ff0f1bd36bc9a227bbb2066330acd8bf25e29d8614ee99e39c",
+        "created": "2020-06-11T22:05:58.382459136Z",
+        "size": 578513505
+    }
+]
+""".lstrip()
+
+
+@pytest.fixture
+def docker_images():
+    from ansible_test._internal.docker_util import docker_images
+    return docker_images
+
+
+@pytest.fixture
+def ansible_test(ansible_test):
+    import ansible_test
+    return ansible_test
+
+
+@pytest.fixture
+def subprocess_error():
+    from ansible_test._internal.util import SubprocessError
+    return SubprocessError
+
+
+@pytest.mark.parametrize(
+    ('returned_items_count', 'patched_dc_stdout'),
+    (
+        (3, (DOCKER_OUTPUT_MULTIPLE, '')),
+        (2, (PODMAN_OUTPUT, '')),
+        (0, ('', '')),
+    ),
+    ids=('docker JSONL', 'podman JSON sequence', 'empty output'))
+def test_docker_images(docker_images, mocker, returned_items_count, patched_dc_stdout):
+    mocker.patch(
+        'ansible_test._internal.docker_util.docker_command',
+        return_value=patched_dc_stdout)
+    ret = docker_images('', 'quay.io/ansible/centos7-test-container')
+    assert len(ret) == returned_items_count
+
+
+def test_podman_fallback(ansible_test, docker_images, subprocess_error, mocker):
+    '''Test podman >2 && <2.2 fallback'''
+
+    cmd = ['docker', 'images', 'quay.io/ansible/centos7-test-container', '--format', '{{json .}}']
+    docker_command_results = [
+        subprocess_error(cmd, status=1, stderr='function "json" not defined'),
+        (PODMAN_OUTPUT, ''),
+    ]
+    mocker.patch(
+        'ansible_test._internal.docker_util.docker_command',
+        side_effect=docker_command_results)
+
+    ret = docker_images('', 'quay.io/ansible/centos7-test-container')
+    calls = [
+        call(
+            '',
+            ['images', 'quay.io/ansible/centos7-test-container', '--format', '{{json .}}'],
+            capture=True,
+            always=True),
+        call(
+            '',
+            ['images', 'quay.io/ansible/centos7-test-container', '--format', 'json'],
+            capture=True,
+            always=True),
+    ]
+    ansible_test._internal.docker_util.docker_command.assert_has_calls(calls)
+    assert len(ret) == 2
+
+
+def test_podman_no_such_image(ansible_test, docker_images, subprocess_error, mocker):
+    '''Test podman "no such image" error'''
+
+    cmd = ['docker', 'images', 'quay.io/ansible/centos7-test-container', '--format', '{{json .}}']
+    exc = subprocess_error(cmd, status=1, stderr='no such image'),
+    mocker.patch(
+        'ansible_test._internal.docker_util.docker_command',
+        side_effect=exc)
+    ret = docker_images('', 'quay.io/ansible/centos7-test-container')
+    assert ret == []