# -*- coding: utf-8 -*-
"""
Unit tests for the docker module
"""
from __future__ import absolute_import, print_function, unicode_literals

import logging

import salt.config
import salt.loader
import salt.modules.dockermod as docker_mod
from salt.exceptions import CommandExecutionError, SaltInvocationError
from salt.ext.six.moves import range
from tests.support.helpers import slowTest
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.mock import MagicMock, Mock, call, patch
from tests.support.unit import TestCase, skipIf

log = logging.getLogger(__name__)


def _docker_py_version():
    try:
        if docker_mod.HAS_DOCKER_PY:
            return docker_mod.docker.version_info
    except AttributeError:
        pass
    return (0,)


class DockerUnitTestCase(TestCase, LoaderModuleMockMixin):
    def fake_run(self, *args, **kwargs):
        print(args, kwargs)
        return "{}"

    def setup_loader_modules(self):
        return {
            docker_mod: {
                "__utils__": {
                    "state.get_sls_opts": MagicMock(
                        return_value={
                            "pillarenv": MagicMock(),
                            "pillar": {},
                            "grains": {},
                        }
                    ),
                    "args.clean_kwargs": lambda **x: x,
                },
                "__salt__": {
                    "config.option": MagicMock(return_value=None),
                    "cmd.run": self.fake_run,
                },
                "__opts__": {"id": "dockermod-unit-test"},
            },
        }

    def test_trans_tar_should_have_grains_in_sls_opts_including_pillar_override(self):
        container_name = "fnord"
        expected_grains = {
            "roscivs": "bottia",
            "fnord": "dronf",
            "salt": "NaCl",
        }
        expected_pillars = {
            "this": {"is": {"my": {"pillar": "data"}}},
        }
        extra_pillar_data = {"some": "extras"}
        fake_trans_tar = MagicMock(return_value=b"hi")
        patch_trans_tar = patch(
            "salt.modules.dockermod._prepare_trans_tar", fake_trans_tar,
        )
        patch_call = patch(
            "salt.modules.dockermod.call", MagicMock(return_value=expected_grains),
        )
        fake_get_pillar = MagicMock()
        fake_get_pillar.compile_pillar.return_value = expected_pillars
        patch_pillar = patch(
            "salt.modules.dockermod.salt.pillar.get_pillar",
            MagicMock(return_value=fake_get_pillar),
        )
        patch_run_all = patch(
            "salt.modules.dockermod.run_all",
            MagicMock(return_value={"retcode": 1, "stderr": "early exit test"}),
        )
        with patch_trans_tar, patch_call, patch_pillar, patch_run_all:
            docker_mod.sls(container_name, pillar=extra_pillar_data)
            # TODO: It would be fine if we could make this test require less magic numbers -W. Werner, 2019-08-27
            actual_sls_opts = fake_trans_tar.call_args[0][1]
            self.assertDictContainsSubset(
                expected_grains,
                actual_sls_opts["grains"],
                "Docker container grains not provided to thin client creation",
            )
            expected_pillars.update(extra_pillar_data)
            self.assertDictContainsSubset(
                expected_pillars,
                actual_sls_opts["pillar"],
                "Docker container pillar not provided to thin client creation",
            )


@skipIf(
    docker_mod.HAS_DOCKER_PY is False,
    "docker-py must be installed to run these tests. Skipping.",
)
class DockerTestCase(TestCase, LoaderModuleMockMixin):
    """
    Validate docker module
    """

    def setup_loader_modules(self):
        utils = salt.loader.utils(
            salt.config.DEFAULT_MINION_OPTS.copy(),
            whitelist=[
                "args",
                "docker",
                "json",
                "state",
                "thin",
                "systemd",
                "path",
                "platform",
            ],
        )
        return {
            docker_mod: {
                "__context__": {"docker.docker_version": ""},
                "__utils__": utils,
            }
        }

    try:
        docker_version = docker_mod.docker.version_info
    except AttributeError:
        docker_version = (0,)

    def setUp(self):
        """
        Ensure we aren't persisting context dunders between tests
        """
        docker_mod.__context__.pop("docker.client", None)

    def test_failed_login(self):
        """
        Check that when docker.login failed a retcode other then 0
        is part of the return.
        """
        client = Mock()
        get_client_mock = MagicMock(return_value=client)
        ref_out = {"stdout": "", "stderr": "login failed", "retcode": 1}
        with patch.dict(
            docker_mod.__pillar__,
            {
                "docker-registries": {
                    "portus.example.com:5000": {
                        "username": "admin",
                        "password": "linux12345",
                        "email": "tux@example.com",
                    }
                }
            },
        ):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                dunder_salt = {
                    "cmd.run_all": MagicMock(return_value=ref_out),
                    "config.get": MagicMock(return_value={}),
                    "config.option": MagicMock(return_value={}),
                }
                with patch.dict(docker_mod.__salt__, dunder_salt):
                    ret = docker_mod.login("portus.example.com:5000")
                    self.assertIn("retcode", ret)
                    self.assertNotEqual(ret["retcode"], 0)

    def test_logout_calls_docker_cli_logout_single(self):
        client = Mock()
        get_client_mock = MagicMock(return_value=client)
        ref_out = {"stdout": "", "stderr": "", "retcode": 0}
        registry_auth_data = {
            "portus.example.com:5000": {
                "username": "admin",
                "password": "linux12345",
                "email": "tux@example.com",
            }
        }
        docker_mock = MagicMock(return_value=ref_out)
        with patch.object(docker_mod, "_get_client", get_client_mock):
            dunder_salt = {
                "config.get": MagicMock(return_value=registry_auth_data),
                "cmd.run_all": docker_mock,
                "config.option": MagicMock(return_value={}),
            }
            with patch.dict(docker_mod.__salt__, dunder_salt):
                ret = docker_mod.logout("portus.example.com:5000")
                assert "retcode" in ret
                assert ret["retcode"] == 0
                docker_mock.assert_called_with(
                    ["docker", "logout", "portus.example.com:5000"],
                    python_shell=False,
                    output_loglevel="quiet",
                )

    def test_logout_calls_docker_cli_logout_all(self):
        client = Mock()
        get_client_mock = MagicMock(return_value=client)
        ref_out = {"stdout": "", "stderr": "", "retcode": 0}
        registry_auth_data = {
            "portus.example.com:5000": {
                "username": "admin",
                "password": "linux12345",
                "email": "tux@example.com",
            },
            "portus2.example.com:5000": {
                "username": "admin",
                "password": "linux12345",
                "email": "tux@example.com",
            },
        }

        docker_mock = MagicMock(return_value=ref_out)
        with patch.object(docker_mod, "_get_client", get_client_mock):
            dunder_salt = {
                "config.get": MagicMock(return_value=registry_auth_data),
                "cmd.run_all": docker_mock,
                "config.option": MagicMock(return_value={}),
            }
            with patch.dict(docker_mod.__salt__, dunder_salt):
                ret = docker_mod.logout()
                assert "retcode" in ret
                assert ret["retcode"] == 0
                assert docker_mock.call_count == 2

    def test_ps_with_host_true(self):
        """
        Check that docker.ps called with host is ``True``,
        include resutlt of ``network.interfaces`` command in returned result.
        """
        client = Mock()
        client.containers = MagicMock(return_value=[])
        get_client_mock = MagicMock(return_value=client)
        network_interfaces = Mock(return_value={"mocked": None})

        with patch.dict(
            docker_mod.__salt__, {"network.interfaces": network_interfaces}
        ):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                ret = docker_mod.ps_(host=True)
                self.assertEqual(ret, {"host": {"interfaces": {"mocked": None}}})

    def test_ps_with_filters(self):
        """
        Check that docker.ps accept filters parameter.
        """
        client = Mock()
        client.containers = MagicMock(return_value=[])
        get_client_mock = MagicMock(return_value=client)

        with patch.object(docker_mod, "_get_client", get_client_mock):
            docker_mod.ps_(filters={"label": "KEY"})
            client.containers.assert_called_once_with(
                all=True, filters={"label": "KEY"}
            )

    @slowTest
    def test_check_mine_cache_is_refreshed_on_container_change_event(self):
        """
        Every command that might modify docker containers state.
        Should trig an update on ``mine.send``
        """
        with patch.object(docker_mod, "_get_exec_driver"):
            client_args_mock = MagicMock(
                return_value={
                    "create_container": [
                        "image",
                        "command",
                        "hostname",
                        "user",
                        "detach",
                        "stdin_open",
                        "tty",
                        "ports",
                        "environment",
                        "volumes",
                        "network_disabled",
                        "name",
                        "entrypoint",
                        "working_dir",
                        "domainname",
                        "cpuset",
                        "host_config",
                        "mac_address",
                        "labels",
                        "volume_driver",
                        "stop_signal",
                        "networking_config",
                        "healthcheck",
                        "stop_timeout",
                    ],
                    "host_config": [
                        "binds",
                        "port_bindings",
                        "lxc_conf",
                        "publish_all_ports",
                        "links",
                        "privileged",
                        "dns",
                        "dns_search",
                        "volumes_from",
                        "network_mode",
                        "restart_policy",
                        "cap_add",
                        "cap_drop",
                        "devices",
                        "extra_hosts",
                        "read_only",
                        "pid_mode",
                        "ipc_mode",
                        "security_opt",
                        "ulimits",
                        "log_config",
                        "mem_limit",
                        "memswap_limit",
                        "mem_reservation",
                        "kernel_memory",
                        "mem_swappiness",
                        "cgroup_parent",
                        "group_add",
                        "cpu_quota",
                        "cpu_period",
                        "blkio_weight",
                        "blkio_weight_device",
                        "device_read_bps",
                        "device_write_bps",
                        "device_read_iops",
                        "device_write_iops",
                        "oom_kill_disable",
                        "shm_size",
                        "sysctls",
                        "tmpfs",
                        "oom_score_adj",
                        "dns_opt",
                        "cpu_shares",
                        "cpuset_cpus",
                        "userns_mode",
                        "pids_limit",
                        "isolation",
                        "auto_remove",
                        "storage_opt",
                    ],
                    "networking_config": [
                        "aliases",
                        "links",
                        "ipv4_address",
                        "ipv6_address",
                        "link_local_ips",
                    ],
                }
            )

            for command_name, args in (
                ("create", ()),
                ("rm_", ()),
                ("kill", ()),
                ("pause", ()),
                ("signal_", ("KILL",)),
                ("start_", ()),
                ("stop", ()),
                ("unpause", ()),
                ("_run", ("command",)),
                ("_script", ("command",)),
            ):
                mine_send = Mock()
                command = getattr(docker_mod, command_name)
                client = MagicMock()
                client.api_version = "1.12"
                with patch.dict(
                    docker_mod.__salt__,
                    {
                        "mine.send": mine_send,
                        "container_resource.run": MagicMock(),
                        "config.get": MagicMock(return_value=True),
                        "config.option": MagicMock(return_value=True),
                        "cp.cache_file": MagicMock(return_value=False),
                    },
                ):
                    with patch.dict(
                        docker_mod.__utils__,
                        {"docker.get_client_args": client_args_mock},
                    ):
                        with patch.object(docker_mod, "_get_client", client):
                            command("container", *args)
                try:
                    mine_send.assert_called_with(
                        "docker.ps", verbose=True, all=True, host=True
                    )
                except AssertionError as exc:
                    raise Exception(
                        "command '{0}' did not call docker.ps with expected "
                        "arguments: {1}".format(command_name, exc)
                    )

    def test_update_mine(self):
        """
        Test the docker.update_mine config option
        """

        def config_get_disabled(val, default):
            return {
                "base_url": docker_mod.NOTSET,
                "version": docker_mod.NOTSET,
                "docker.url": docker_mod.NOTSET,
                "docker.version": docker_mod.NOTSET,
                "docker.machine": docker_mod.NOTSET,
                "docker.update_mine": False,
            }[val]

        def config_get_enabled(val, default):
            return {
                "base_url": docker_mod.NOTSET,
                "version": docker_mod.NOTSET,
                "docker.url": docker_mod.NOTSET,
                "docker.version": docker_mod.NOTSET,
                "docker.machine": docker_mod.NOTSET,
                "docker.update_mine": True,
            }[val]

        mine_mock = Mock()
        dunder_salt = {
            "config.get": MagicMock(side_effect=config_get_disabled),
            "config.option": MagicMock(return_value=False),
            "mine.send": mine_mock,
        }
        with patch.dict(docker_mod.__salt__, dunder_salt), patch.dict(
            docker_mod.__context__, {"docker.client": Mock()}
        ), patch.object(docker_mod, "state", MagicMock(return_value="stopped")):
            docker_mod.stop("foo", timeout=1)
            mine_mock.assert_not_called()

        with patch.dict(docker_mod.__salt__, dunder_salt), patch.dict(
            docker_mod.__context__, {"docker.client": Mock()}
        ), patch.object(docker_mod, "state", MagicMock(return_value="stopped")):
            dunder_salt["config.get"].side_effect = config_get_enabled
            dunder_salt["config.option"].return_value = True
            docker_mod.stop("foo", timeout=1)
            self.assert_called_once(mine_mock)

    @skipIf(
        _docker_py_version() < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_list_networks(self, *args):
        """
        test list networks.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        host_config = {}
        client = Mock()
        client.api_version = "1.21"
        client.networks = Mock(
            return_value=[{"Name": "foo", "Id": "01234", "Containers": {}}]
        )
        get_client_mock = MagicMock(return_value=client)
        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.networks(names=["foo"], ids=["01234"])
        client.networks.assert_called_once_with(names=["foo"], ids=["01234"])

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_create_network(self, *args):
        """
        test create network.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        host_config = {}
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.create_network(
                    "foo",
                    driver="bridge",
                    driver_opts={},
                    gateway="192.168.0.1",
                    ip_range="192.168.0.128/25",
                    subnet="192.168.0.0/24",
                )
        client.create_network.assert_called_once_with(
            "foo",
            driver="bridge",
            options={},
            ipam={
                "Config": [
                    {
                        "Gateway": "192.168.0.1",
                        "IPRange": "192.168.0.128/25",
                        "Subnet": "192.168.0.0/24",
                    }
                ],
                "Driver": "default",
            },
            check_duplicate=True,
        )

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_remove_network(self, *args):
        """
        test remove network.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        host_config = {}
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.remove_network("foo")
        client.remove_network.assert_called_once_with("foo")

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_inspect_network(self, *args):
        """
        test inspect network.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        host_config = {}
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.inspect_network("foo")
        client.inspect_network.assert_called_once_with("foo")

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_connect_container_to_network(self, *args):
        """
        test connect_container_to_network
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        host_config = {}
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        context = {"docker.exec_driver": "docker-exec"}

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.dict(docker_mod.__context__, context):
                with patch.object(docker_mod, "_get_client", get_client_mock):
                    docker_mod.connect_container_to_network("container", "foo")
        client.connect_container_to_network.assert_called_once_with("container", "foo")

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_disconnect_container_from_network(self, *args):
        """
        test disconnect_container_from_network
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        host_config = {}
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.disconnect_container_from_network("container", "foo")
        client.disconnect_container_from_network.assert_called_once_with(
            "container", "foo"
        )

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_list_volumes(self, *args):
        """
        test list volumes.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.volumes(filters={"dangling": [True]},)
        client.volumes.assert_called_once_with(filters={"dangling": [True]},)

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_create_volume(self, *args):
        """
        test create volume.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.create_volume(
                    "foo", driver="bridge", driver_opts={},
                )
        client.create_volume.assert_called_once_with(
            "foo", driver="bridge", driver_opts={},
        )

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_remove_volume(self, *args):
        """
        test remove volume.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.remove_volume("foo")
        client.remove_volume.assert_called_once_with("foo")

    @skipIf(
        docker_version < (1, 5, 0),
        "docker module must be installed to run this test or is too old. >=1.5.0",
    )
    def test_inspect_volume(self, *args):
        """
        test inspect volume.
        """
        __salt__ = {
            "config.get": Mock(),
            "mine.send": Mock(),
        }
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        with patch.dict(docker_mod.__dict__, {"__salt__": __salt__}):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod.inspect_volume("foo")
        client.inspect_volume.assert_called_once_with("foo")

    def test_wait_success(self):
        client = Mock()
        client.api_version = "1.21"
        client.wait = Mock(return_value=0)
        get_client_mock = MagicMock(return_value=client)

        docker_inspect_container = Mock(
            side_effect=[{"State": {"Running": True}}, {"State": {"Stopped": True}}]
        )
        with patch.object(docker_mod, "inspect_container", docker_inspect_container):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod._clear_context()
                result = docker_mod.wait("foo")
        self.assertEqual(
            result,
            {
                "result": True,
                "exit_status": 0,
                "state": {"new": "stopped", "old": "running"},
            },
        )

    def test_wait_fails_already_stopped(self):
        client = Mock()
        client.api_version = "1.21"
        client.wait = Mock(return_value=0)
        get_client_mock = MagicMock(return_value=client)

        docker_inspect_container = Mock(
            side_effect=[{"State": {"Stopped": True}}, {"State": {"Stopped": True}}]
        )
        with patch.object(docker_mod, "inspect_container", docker_inspect_container):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod._clear_context()
                result = docker_mod.wait("foo")
        self.assertEqual(
            result,
            {
                "result": False,
                "comment": "Container 'foo' already stopped",
                "exit_status": 0,
                "state": {"new": "stopped", "old": "stopped"},
            },
        )

    def test_wait_success_already_stopped(self):
        client = Mock()
        client.api_version = "1.21"
        client.wait = Mock(return_value=0)
        get_client_mock = MagicMock(return_value=client)

        docker_inspect_container = Mock(
            side_effect=[{"State": {"Stopped": True}}, {"State": {"Stopped": True}}]
        )
        with patch.object(docker_mod, "inspect_container", docker_inspect_container):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod._clear_context()
                result = docker_mod.wait("foo", ignore_already_stopped=True)
        self.assertEqual(
            result,
            {
                "result": True,
                "comment": "Container 'foo' already stopped",
                "exit_status": 0,
                "state": {"new": "stopped", "old": "stopped"},
            },
        )

    def test_wait_success_absent_container(self):
        client = Mock()
        client.api_version = "1.21"
        get_client_mock = MagicMock(return_value=client)

        docker_inspect_container = Mock(side_effect=CommandExecutionError)
        with patch.object(docker_mod, "inspect_container", docker_inspect_container):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod._clear_context()
                result = docker_mod.wait("foo", ignore_already_stopped=True)
        self.assertEqual(result, {"result": True, "comment": "Container 'foo' absent"})

    def test_wait_fails_on_exit_status(self):
        client = Mock()
        client.api_version = "1.21"
        client.wait = Mock(return_value=1)
        get_client_mock = MagicMock(return_value=client)

        docker_inspect_container = Mock(
            side_effect=[{"State": {"Running": True}}, {"State": {"Stopped": True}}]
        )
        with patch.object(docker_mod, "inspect_container", docker_inspect_container):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod._clear_context()
                result = docker_mod.wait("foo", fail_on_exit_status=True)
        self.assertEqual(
            result,
            {
                "result": False,
                "exit_status": 1,
                "state": {"new": "stopped", "old": "running"},
            },
        )

    def test_wait_fails_on_exit_status_and_already_stopped(self):
        client = Mock()
        client.api_version = "1.21"
        client.wait = Mock(return_value=1)
        get_client_mock = MagicMock(return_value=client)

        docker_inspect_container = Mock(
            side_effect=[{"State": {"Stopped": True}}, {"State": {"Stopped": True}}]
        )
        with patch.object(docker_mod, "inspect_container", docker_inspect_container):
            with patch.object(docker_mod, "_get_client", get_client_mock):
                docker_mod._clear_context()
                result = docker_mod.wait(
                    "foo", ignore_already_stopped=True, fail_on_exit_status=True
                )
        self.assertEqual(
            result,
            {
                "result": False,
                "comment": "Container 'foo' already stopped",
                "exit_status": 1,
                "state": {"new": "stopped", "old": "stopped"},
            },
        )

    def test_sls_build(self, *args):
        """
        test build sls image.
        """
        docker_start_mock = MagicMock(return_value={})
        docker_create_mock = MagicMock(return_value={"Id": "ID", "Name": "NAME"})
        docker_stop_mock = MagicMock(
            return_value={"state": {"old": "running", "new": "stopped"}, "result": True}
        )
        docker_rm_mock = MagicMock(return_value={})
        docker_commit_mock = MagicMock(
            return_value={"Id": "ID2", "Image": "foo", "Time_Elapsed": 42}
        )

        docker_sls_mock = MagicMock(
            return_value={
                "file_|-/etc/test.sh_|-/etc/test.sh_|-managed": {
                    "comment": "File /etc/test.sh is in the correct state",
                    "name": "/etc/test.sh",
                    "start_time": "07:04:26.834792",
                    "result": True,
                    "duration": 13.492,
                    "__run_num__": 0,
                    "changes": {},
                },
                "test_|-always-passes_|-foo_|-succeed_without_changes": {
                    "comment": "Success!",
                    "name": "foo",
                    "start_time": "07:04:26.848915",
                    "result": True,
                    "duration": 0.363,
                    "__run_num__": 1,
                    "changes": {},
                },
            }
        )

        ret = None
        with patch.object(docker_mod, "start_", docker_start_mock), patch.object(
            docker_mod, "create", docker_create_mock
        ), patch.object(docker_mod, "stop", docker_stop_mock), patch.object(
            docker_mod, "commit", docker_commit_mock
        ), patch.object(
            docker_mod, "sls", docker_sls_mock
        ), patch.object(
            docker_mod, "rm_", docker_rm_mock
        ):
            ret = docker_mod.sls_build("foo", mods="foo")
        docker_create_mock.assert_called_once_with(
            cmd="sleep infinity", image="opensuse/python", interactive=True, tty=True
        )
        docker_start_mock.assert_called_once_with("ID")
        docker_sls_mock.assert_called_once_with("ID", "foo")
        docker_stop_mock.assert_called_once_with("ID")
        docker_rm_mock.assert_called_once_with("ID")
        docker_commit_mock.assert_called_once_with("ID", "foo", tag="latest")
        self.assertEqual({"Id": "ID2", "Image": "foo", "Time_Elapsed": 42}, ret)

    def test_sls_build_dryrun(self, *args):
        """
        test build sls image in dryrun mode.
        """
        docker_start_mock = MagicMock(return_value={})
        docker_create_mock = MagicMock(return_value={"Id": "ID", "Name": "NAME"})
        docker_stop_mock = MagicMock(
            return_value={"state": {"old": "running", "new": "stopped"}, "result": True}
        )
        docker_rm_mock = MagicMock(return_value={})

        docker_sls_mock = MagicMock(
            return_value={
                "file_|-/etc/test.sh_|-/etc/test.sh_|-managed": {
                    "comment": "File /etc/test.sh is in the correct state",
                    "name": "/etc/test.sh",
                    "start_time": "07:04:26.834792",
                    "result": True,
                    "duration": 13.492,
                    "__run_num__": 0,
                    "changes": {},
                },
                "test_|-always-passes_|-foo_|-succeed_without_changes": {
                    "comment": "Success!",
                    "name": "foo",
                    "start_time": "07:04:26.848915",
                    "result": True,
                    "duration": 0.363,
                    "__run_num__": 1,
                    "changes": {},
                },
            }
        )

        ret = None
        with patch.object(docker_mod, "start_", docker_start_mock), patch.object(
            docker_mod, "create", docker_create_mock
        ), patch.object(docker_mod, "stop", docker_stop_mock), patch.object(
            docker_mod, "rm_", docker_rm_mock
        ), patch.object(
            docker_mod, "sls", docker_sls_mock
        ):
            ret = docker_mod.sls_build("foo", mods="foo", dryrun=True)
        docker_create_mock.assert_called_once_with(
            cmd="sleep infinity", image="opensuse/python", interactive=True, tty=True
        )
        docker_start_mock.assert_called_once_with("ID")
        docker_sls_mock.assert_called_once_with("ID", "foo")
        docker_stop_mock.assert_called_once_with("ID")
        docker_rm_mock.assert_called_once_with("ID")
        self.assertEqual(
            {
                "file_|-/etc/test.sh_|-/etc/test.sh_|-managed": {
                    "comment": "File /etc/test.sh is in the correct state",
                    "name": "/etc/test.sh",
                    "start_time": "07:04:26.834792",
                    "result": True,
                    "duration": 13.492,
                    "__run_num__": 0,
                    "changes": {},
                },
                "test_|-always-passes_|-foo_|-succeed_without_changes": {
                    "comment": "Success!",
                    "name": "foo",
                    "start_time": "07:04:26.848915",
                    "result": True,
                    "duration": 0.363,
                    "__run_num__": 1,
                    "changes": {},
                },
            },
            ret,
        )

    @slowTest
    def test_call_success(self):
        """
        test module calling inside containers
        """
        ret = None
        docker_run_all_mock = MagicMock(
            return_value={
                "retcode": 0,
                "stdout": '{"retcode": 0, "comment": "container cmd"}',
                "stderr": "err",
            }
        )
        docker_copy_to_mock = MagicMock(return_value={"retcode": 0})
        docker_config_mock = MagicMock(return_value="")
        client = Mock()
        client.put_archive = Mock()
        get_client_mock = MagicMock(return_value=client)

        context = {"docker.exec_driver": "docker-exec"}
        salt_dunder = {"config.option": docker_config_mock}

        with patch.object(docker_mod, "run_all", docker_run_all_mock), patch.object(
            docker_mod, "copy_to", docker_copy_to_mock
        ), patch.object(docker_mod, "_get_client", get_client_mock), patch.dict(
            docker_mod.__opts__, {"cachedir": "/tmp"}
        ), patch.dict(
            docker_mod.__salt__, salt_dunder
        ), patch.dict(
            docker_mod.__context__, context
        ):
            # call twice to verify tmp path later
            for i in range(2):
                ret = docker_mod.call("ID", "test.arg", 1, 2, arg1="val1")

        # Check that the directory is different each time
        # [ call(name, [args]), ...
        self.maxDiff = None
        self.assertIn("mkdir", docker_run_all_mock.mock_calls[0][1][1])
        self.assertIn("mkdir", docker_run_all_mock.mock_calls[4][1][1])
        self.assertNotEqual(
            docker_run_all_mock.mock_calls[0][1][1],
            docker_run_all_mock.mock_calls[4][1][1],
        )

        self.assertIn("salt-call", docker_run_all_mock.mock_calls[2][1][1])
        self.assertIn("salt-call", docker_run_all_mock.mock_calls[6][1][1])
        self.assertNotEqual(
            docker_run_all_mock.mock_calls[2][1][1],
            docker_run_all_mock.mock_calls[6][1][1],
        )

        # check thin untar
        self.assertIn("tarfile", docker_run_all_mock.mock_calls[1][1][1])
        self.assertIn("tarfile", docker_run_all_mock.mock_calls[5][1][1])
        self.assertNotEqual(
            docker_run_all_mock.mock_calls[1][1][1],
            docker_run_all_mock.mock_calls[5][1][1],
        )

        # check directory cleanup
        self.assertIn("rm -rf", docker_run_all_mock.mock_calls[3][1][1])
        self.assertIn("rm -rf", docker_run_all_mock.mock_calls[7][1][1])
        self.assertNotEqual(
            docker_run_all_mock.mock_calls[3][1][1],
            docker_run_all_mock.mock_calls[7][1][1],
        )

        self.assertEqual({"retcode": 0, "comment": "container cmd"}, ret)

    def test_images_with_empty_tags(self):
        """
        docker 1.12 reports also images without tags with `null`.
        """
        client = Mock()
        client.api_version = "1.24"
        client.images = Mock(
            return_value=[
                {"Id": "sha256:abcde", "RepoTags": None},
                {"Id": "sha256:abcdef"},
                {"Id": "sha256:abcdefg", "RepoTags": ["image:latest"]},
            ]
        )
        get_client_mock = MagicMock(return_value=client)

        with patch.object(docker_mod, "_get_client", get_client_mock):
            docker_mod._clear_context()
            result = docker_mod.images()
        self.assertEqual(result, {"sha256:abcdefg": {"RepoTags": ["image:latest"]}})

    def test_compare_container_image_id_resolution(self):
        """
        Test comparing two containers when one's inspect output is an ID and
        not formatted in image:tag notation.
        """

        def _inspect_container_effect(id_):
            return {
                "container1": {
                    "Config": {"Image": "realimage:latest"},
                    "HostConfig": {},
                },
                "container2": {"Config": {"Image": "image_id"}, "HostConfig": {}},
            }[id_]

        def _inspect_image_effect(id_):
            return {
                "realimage:latest": {"Id": "image_id"},
                "image_id": {"Id": "image_id"},
            }[id_]

        inspect_container_mock = MagicMock(side_effect=_inspect_container_effect)
        inspect_image_mock = MagicMock(side_effect=_inspect_image_effect)

        with patch.object(docker_mod, "inspect_container", inspect_container_mock):
            with patch.object(docker_mod, "inspect_image", inspect_image_mock):
                ret = docker_mod.compare_containers("container1", "container2")
                self.assertEqual(ret, {})

    def test_compare_container_ulimits_order(self):
        """
        Test comparing two containers when the order of the Ulimits HostConfig
        values are different, but the values are the same.
        """

        def _inspect_container_effect(id_):
            return {
                "container1": {
                    "Config": {},
                    "HostConfig": {
                        "Ulimits": [
                            {"Hard": -1, "Soft": -1, "Name": "core"},
                            {"Hard": 65536, "Soft": 65536, "Name": "nofile"},
                        ]
                    },
                },
                "container2": {
                    "Config": {},
                    "HostConfig": {
                        "Ulimits": [
                            {"Hard": 65536, "Soft": 65536, "Name": "nofile"},
                            {"Hard": -1, "Soft": -1, "Name": "core"},
                        ]
                    },
                },
            }[id_]

        inspect_container_mock = MagicMock(side_effect=_inspect_container_effect)

        with patch.object(docker_mod, "inspect_container", inspect_container_mock):
            # pylint: disable=not-callable
            ret = docker_mod.compare_container("container1", "container2")
            # pylint: enable=not-callable
            self.assertEqual(ret, {})

    def test_compare_container_env_order(self):
        """
        Test comparing two containers when the order of the Env HostConfig
        values are different, but the values are the same.
        """

        def _inspect_container_effect(id_):
            return {
                "container1": {
                    "Config": {},
                    "HostConfig": {"Env": ["FOO=bar", "HELLO=world"]},
                },
                "container2": {
                    "Config": {},
                    "HostConfig": {"Env": ["HELLO=world", "FOO=bar"]},
                },
            }[id_]

        inspect_container_mock = MagicMock(side_effect=_inspect_container_effect)

        with patch.object(docker_mod, "inspect_container", inspect_container_mock):
            # pylint: disable=not-callable
            ret = docker_mod.compare_container("container1", "container2")
            # pylint: enable=not-callable
            self.assertEqual(ret, {})

    def test_resolve_tag(self):
        """
        Test the resolve_tag function. It runs docker.insect_image on the image
        name passed and then looks for the RepoTags key in the result
        """
        id_ = "sha256:6ad733544a6317992a6fac4eb19fe1df577d4dec7529efec28a5bd0edad0fd30"
        tags = ["foo:latest", "foo:bar"]
        mock_tagged = MagicMock(return_value={"Id": id_, "RepoTags": tags})
        mock_untagged = MagicMock(return_value={"Id": id_, "RepoTags": []})
        mock_unexpected = MagicMock(return_value={"Id": id_})
        mock_not_found = MagicMock(side_effect=CommandExecutionError())

        with patch.object(docker_mod, "inspect_image", mock_tagged):
            self.assertEqual(docker_mod.resolve_tag("foo"), tags[0])
            self.assertEqual(docker_mod.resolve_tag("foo", all=True), tags)

        with patch.object(docker_mod, "inspect_image", mock_untagged):
            self.assertEqual(docker_mod.resolve_tag("foo"), id_)
            self.assertEqual(docker_mod.resolve_tag("foo", all=True), [id_])

        with patch.object(docker_mod, "inspect_image", mock_unexpected):
            self.assertEqual(docker_mod.resolve_tag("foo"), id_)
            self.assertEqual(docker_mod.resolve_tag("foo", all=True), [id_])

        with patch.object(docker_mod, "inspect_image", mock_not_found):
            self.assertIs(docker_mod.resolve_tag("foo"), False)
            self.assertIs(docker_mod.resolve_tag("foo", all=True), False)

    def test_prune(self):
        """
        Test the prune function
        """

        def _run(**kwargs):
            side_effect = kwargs.pop("side_effect", None)
            # No arguments passed, we should be pruning everything but volumes
            client = Mock()
            if side_effect is not None:
                client.side_effect = side_effect
            with patch.object(docker_mod, "_client_wrapper", client):
                docker_mod.prune(**kwargs)
            return client

        # Containers only, no filters
        client = _run(containers=True)
        client.assert_called_once_with("prune_containers", filters={})

        # Containers only, with filters
        client = _run(containers=True, until="24h", label="foo,bar=baz")
        client.assert_called_once_with(
            "prune_containers", filters={"until": ["24h"], "label": ["foo", "bar=baz"]}
        )

        # Images only, no filters
        client = _run(images=True)
        client.assert_called_once_with("prune_images", filters={})

        # Images only, with filters
        client = _run(images=True, dangling=True, until="24h", label="foo,bar=baz")
        client.assert_called_once_with(
            "prune_images",
            filters={"dangling": True, "until": ["24h"], "label": ["foo", "bar=baz"]},
        )

        # Networks only, no filters
        client = _run(networks=True)
        client.assert_called_once_with("prune_networks", filters={})

        # Networks only, with filters
        client = _run(networks=True, until="24h", label="foo,bar=baz")
        client.assert_called_once_with(
            "prune_networks", filters={"until": ["24h"], "label": ["foo", "bar=baz"]}
        )

        # Volumes only, no filters
        client = _run(system=False, volumes=True)
        client.assert_called_once_with("prune_volumes", filters={})

        # Volumes only, with filters
        client = _run(system=False, volumes=True, label="foo,bar=baz")
        client.assert_called_once_with(
            "prune_volumes", filters={"label": ["foo", "bar=baz"]}
        )

        # Containers and images, no filters
        client = _run(containers=True, images=True)
        self.assertEqual(
            client.call_args_list,
            [call("prune_containers", filters={}), call("prune_images", filters={})],
        )

        # Containers and images, with filters
        client = _run(containers=True, images=True, until="24h", label="foo,bar=baz")
        self.assertEqual(
            client.call_args_list,
            [
                call(
                    "prune_containers",
                    filters={"until": ["24h"], "label": ["foo", "bar=baz"]},
                ),
                call(
                    "prune_images",
                    filters={"until": ["24h"], "label": ["foo", "bar=baz"]},
                ),
            ],
        )

        # System, no volumes, no filters, assuming prune_build in docker-py
        client = _run(system=True)
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={}),
                call("prune_images", filters={}),
                call("prune_networks", filters={}),
                call("prune_build", filters={}),
            ],
        )

        # System, no volumes, no filters, assuming prune_build not in docker-py
        client = _run(
            system=True,
            side_effect=[None, None, None, SaltInvocationError(), None, None, None],
        )
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={}),
                call("prune_images", filters={}),
                call("prune_networks", filters={}),
                call("prune_build", filters={}),
                call("_url", "/build/prune"),
                call("_post", None),
                call("_result", None, True),
            ],
        )

        # System, no volumes, with filters, assuming prune_build in docker-py
        client = _run(system=True, label="foo,bar=baz")
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={"label": ["foo", "bar=baz"]}),
                call("prune_images", filters={"label": ["foo", "bar=baz"]}),
                call("prune_networks", filters={"label": ["foo", "bar=baz"]}),
                call("prune_build", filters={"label": ["foo", "bar=baz"]}),
            ],
        )

        # System, no volumes, with filters, assuming prune_build not in docker-py
        client = _run(
            system=True,
            label="foo,bar=baz",
            side_effect=[None, None, None, SaltInvocationError(), None, None, None],
        )
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={"label": ["foo", "bar=baz"]}),
                call("prune_images", filters={"label": ["foo", "bar=baz"]}),
                call("prune_networks", filters={"label": ["foo", "bar=baz"]}),
                call("prune_build", filters={"label": ["foo", "bar=baz"]}),
                call("_url", "/build/prune"),
                call("_post", None),
                call("_result", None, True),
            ],
        )

        # System and volumes, no filters, assuming prune_build in docker-py
        client = _run(system=True, volumes=True)
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={}),
                call("prune_images", filters={}),
                call("prune_networks", filters={}),
                call("prune_build", filters={}),
                call("prune_volumes", filters={}),
            ],
        )

        # System and volumes, no filters, assuming prune_build not in docker-py
        client = _run(
            system=True,
            volumes=True,
            side_effect=[
                None,
                None,
                None,
                SaltInvocationError(),
                None,
                None,
                None,
                None,
            ],
        )
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={}),
                call("prune_images", filters={}),
                call("prune_networks", filters={}),
                call("prune_build", filters={}),
                call("_url", "/build/prune"),
                call("_post", None),
                call("_result", None, True),
                call("prune_volumes", filters={}),
            ],
        )

        # System and volumes with filters, assuming prune_build in docker-py
        client = _run(system=True, volumes=True, label="foo,bar=baz")
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={"label": ["foo", "bar=baz"]}),
                call("prune_images", filters={"label": ["foo", "bar=baz"]}),
                call("prune_networks", filters={"label": ["foo", "bar=baz"]}),
                call("prune_build", filters={"label": ["foo", "bar=baz"]}),
                call("prune_volumes", filters={"label": ["foo", "bar=baz"]}),
            ],
        )

        # System and volumes, with filters, assuming prune_build not in docker-py
        client = _run(
            system=True,
            volumes=True,
            label="foo,bar=baz",
            side_effect=[
                None,
                None,
                None,
                SaltInvocationError(),
                None,
                None,
                None,
                None,
            ],
        )
        self.assertEqual(
            client.call_args_list,
            [
                call("prune_containers", filters={"label": ["foo", "bar=baz"]}),
                call("prune_images", filters={"label": ["foo", "bar=baz"]}),
                call("prune_networks", filters={"label": ["foo", "bar=baz"]}),
                call("prune_build", filters={"label": ["foo", "bar=baz"]}),
                call("_url", "/build/prune"),
                call("_post", None),
                call("_result", None, True),
                call("prune_volumes", filters={"label": ["foo", "bar=baz"]}),
            ],
        )

    def test_port(self):
        """
        Test docker.port function. Note that this test case does not test what
        happens when a specific container name is passed and that container
        does not exist. When that happens, the Docker API will just raise a 404
        error. Since we're using as side_effect to mock
        docker.inspect_container, it would be meaningless to code raising an
        exception into it and then test that we raised that exception.
        """
        ports = {
            "foo": {
                "5555/tcp": [{"HostIp": "0.0.0.0", "HostPort": "32768"}],
                "6666/tcp": [{"HostIp": "0.0.0.0", "HostPort": "32769"}],
            },
            "bar": {
                "4444/udp": [{"HostIp": "0.0.0.0", "HostPort": "32767"}],
                "5555/tcp": [{"HostIp": "0.0.0.0", "HostPort": "32768"}],
                "6666/tcp": [{"HostIp": "0.0.0.0", "HostPort": "32769"}],
            },
            "baz": {
                "5555/tcp": [{"HostIp": "0.0.0.0", "HostPort": "32768"}],
                "6666/udp": [{"HostIp": "0.0.0.0", "HostPort": "32769"}],
            },
        }
        list_mock = MagicMock(return_value=["bar", "baz", "foo"])
        inspect_mock = MagicMock(
            side_effect=lambda x: {"NetworkSettings": {"Ports": ports.get(x)}}
        )
        with patch.object(docker_mod, "list_containers", list_mock), patch.object(
            docker_mod, "inspect_container", inspect_mock
        ):

            # Test with specific container name
            ret = docker_mod.port("foo")
            self.assertEqual(ret, ports["foo"])

            # Test with specific container name and filtering on port
            ret = docker_mod.port("foo", private_port="5555/tcp")
            self.assertEqual(ret, {"5555/tcp": ports["foo"]["5555/tcp"]})

            # Test using pattern expression
            ret = docker_mod.port("ba*")
            self.assertEqual(ret, {"bar": ports["bar"], "baz": ports["baz"]})
            ret = docker_mod.port("ba?")
            self.assertEqual(ret, {"bar": ports["bar"], "baz": ports["baz"]})
            ret = docker_mod.port("ba[rz]")
            self.assertEqual(ret, {"bar": ports["bar"], "baz": ports["baz"]})

            # Test using pattern expression and port filtering
            ret = docker_mod.port("ba*", private_port="6666/tcp")
            self.assertEqual(
                ret, {"bar": {"6666/tcp": ports["bar"]["6666/tcp"]}, "baz": {}}
            )
            ret = docker_mod.port("ba?", private_port="6666/tcp")
            self.assertEqual(
                ret, {"bar": {"6666/tcp": ports["bar"]["6666/tcp"]}, "baz": {}}
            )
            ret = docker_mod.port("ba[rz]", private_port="6666/tcp")
            self.assertEqual(
                ret, {"bar": {"6666/tcp": ports["bar"]["6666/tcp"]}, "baz": {}}
            )
            ret = docker_mod.port("*")
            self.assertEqual(ret, ports)
            ret = docker_mod.port("*", private_port="5555/tcp")
            self.assertEqual(
                ret,
                {
                    "foo": {"5555/tcp": ports["foo"]["5555/tcp"]},
                    "bar": {"5555/tcp": ports["bar"]["5555/tcp"]},
                    "baz": {"5555/tcp": ports["baz"]["5555/tcp"]},
                },
            )
            ret = docker_mod.port("*", private_port=6666)
            self.assertEqual(
                ret,
                {
                    "foo": {"6666/tcp": ports["foo"]["6666/tcp"]},
                    "bar": {"6666/tcp": ports["bar"]["6666/tcp"]},
                    "baz": {"6666/udp": ports["baz"]["6666/udp"]},
                },
            )
            ret = docker_mod.port("*", private_port="6666/tcp")
            self.assertEqual(
                ret,
                {
                    "foo": {"6666/tcp": ports["foo"]["6666/tcp"]},
                    "bar": {"6666/tcp": ports["bar"]["6666/tcp"]},
                    "baz": {},
                },
            )
