From 6e7d09b76424cc0b923cb3ad869c44b5250e6708 Mon Sep 17 00:00:00 2001 From: Ian Woodard <17186604+IanWoodard@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:37:39 -0800 Subject: [PATCH 1/9] feat(reset): Adding reset command --- devservices/commands/purge.py | 4 +- devservices/commands/reset.py | 110 ++++++++++++++++++++++++++++++++++ devservices/main.py | 2 + devservices/utils/docker.py | 12 ++-- tests/commands/test_purge.py | 14 ++--- tests/utils/test_docker.py | 6 +- 6 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 devservices/commands/reset.py diff --git a/devservices/commands/purge.py b/devservices/commands/purge.py index a63f7e8e..733e1598 100644 --- a/devservices/commands/purge.py +++ b/devservices/commands/purge.py @@ -40,7 +40,9 @@ def purge(_args: Namespace) -> None: state.clear_state() try: - devservices_containers = get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL) + devservices_containers = get_matching_containers( + [DEVSERVICES_ORCHESTRATOR_LABEL] + ) except DockerDaemonNotRunningError as e: console.warning(str(e)) return diff --git a/devservices/commands/reset.py b/devservices/commands/reset.py new file mode 100644 index 00000000..f76dc793 --- /dev/null +++ b/devservices/commands/reset.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from argparse import _SubParsersAction +from argparse import ArgumentParser +from argparse import Namespace + +from sentry_sdk import capture_exception + +from devservices.commands.down import down +from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL +from devservices.exceptions import DockerDaemonNotRunningError +from devservices.exceptions import DockerError +from devservices.utils.console import Console +from devservices.utils.console import Status +from devservices.utils.dependencies import construct_dependency_graph +from devservices.utils.docker import get_matching_containers +from devservices.utils.docker import get_volumes_for_containers +from devservices.utils.docker import remove_docker_resources +from devservices.utils.docker import stop_containers +from devservices.utils.services import find_matching_service +from devservices.utils.state import State +from devservices.utils.state import StateTables + + +def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None: + parser = subparsers.add_parser("reset", help="Reset a service's volumes") + parser.add_argument( + "service_name", + help="Name of the service to reset volumes for", + nargs="?", + default=None, + ) + parser.set_defaults(func=reset) + + +def reset(args: Namespace) -> None: + """Reset a specified service's volumes.""" + console = Console() + service_name = args.service_name + + try: + matching_containers = get_matching_containers( + [ + DEVSERVICES_ORCHESTRATOR_LABEL, + f"com.docker.compose.service={args.service_name}", + ] + ) + except DockerDaemonNotRunningError as e: + console.warning(str(e)) + return + except DockerError as e: + console.failure(f"Failed to get matching containers {e.stderr}") + exit(1) + + if len(matching_containers) == 0: + console.failure(f"No containers found for {service_name}") + exit(1) + + try: + matching_volumes = get_volumes_for_containers(matching_containers) + except DockerError as e: + console.failure(f"Failed to get matching volumes {e.stderr}") + exit(1) + + if len(matching_volumes) == 0: + console.failure(f"No volumes found for {service_name}") + exit(1) + + state = State() + started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES)) + starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES)) + active_service_names = starting_services.union(started_services) + + # TODO: Should we add threading here to speed up the process? + for active_service_name in active_service_names: + active_service = find_matching_service(active_service_name) + starting_active_modes = state.get_active_modes_for_service( + active_service_name, StateTables.STARTING_SERVICES + ) + started_active_modes = state.get_active_modes_for_service( + active_service_name, StateTables.STARTED_SERVICES + ) + active_modes = starting_active_modes or started_active_modes + dependency_graph = construct_dependency_graph(active_service, active_modes) + if service_name in dependency_graph.graph: + console.warning( + f"Bringing down {active_service_name} in order to safely reset {service_name}" + ) + down(Namespace(service_name=active_service_name)) + + with Status( + lambda: console.warning(f"Resetting docker volumes for {service_name}"), + lambda: console.success(f"Docker volumes have been reset for {service_name}"), + ): + try: + stop_containers(matching_containers, should_remove=True) + except DockerError as e: + console.failure( + f"Failed to stop and remove {', '.join(matching_containers)} {service_name} {e.stderr}" + ) + capture_exception(e) + exit(1) + try: + remove_docker_resources("volume", list(matching_volumes)) + except DockerError as e: + console.failure( + f"Failed to remove volumes {', '.join(matching_volumes)} for {service_name} {e}" + ) + capture_exception(e) + exit(1) diff --git a/devservices/main.py b/devservices/main.py index c930132b..f219bd43 100644 --- a/devservices/main.py +++ b/devservices/main.py @@ -20,6 +20,7 @@ from devservices.commands import list_services from devservices.commands import logs from devservices.commands import purge +from devservices.commands import reset from devservices.commands import status from devservices.commands import up from devservices.commands import update @@ -87,6 +88,7 @@ def main() -> None: logs.add_parser(subparsers) update.add_parser(subparsers) purge.add_parser(subparsers) + reset.add_parser(subparsers) args = parser.parse_args() diff --git a/devservices/utils/docker.py b/devservices/utils/docker.py index 54bc89b4..5c52fe68 100644 --- a/devservices/utils/docker.py +++ b/devservices/utils/docker.py @@ -78,11 +78,14 @@ def wait_for_healthy(container_name: str, status: Status) -> None: raise ContainerHealthcheckFailedError(container_name, HEALTHCHECK_TIMEOUT) -def get_matching_containers(label: str) -> list[str]: +def get_matching_containers(labels: list[str]) -> list[str]: """ Returns a list of container names with the given label """ check_docker_daemon_running() + filters = [] + for label in labels: + filters.extend(["--filter", f"label={label}"]) try: return ( subprocess.check_output( @@ -91,9 +94,8 @@ def get_matching_containers(label: str) -> list[str]: "ps", "-a", "-q", - "--filter", - f"label={label}", - ], + ] + + filters, text=True, stderr=subprocess.DEVNULL, ) @@ -102,7 +104,7 @@ def get_matching_containers(label: str) -> list[str]: ) except subprocess.CalledProcessError as e: raise DockerError( - command=f"docker ps -q --filter label={label}", + command=f"docker ps -a -q {' '.join(filters)}", returncode=e.returncode, stdout=e.stdout, stderr=e.stderr, diff --git a/tests/commands/test_purge.py b/tests/commands/test_purge.py index f967a632..e2f4d5c0 100644 --- a/tests/commands/test_purge.py +++ b/tests/commands/test_purge.py @@ -58,7 +58,7 @@ def test_purge_docker_daemon_not_running( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_not_called() mock_stop_containers.assert_not_called() @@ -117,7 +117,7 @@ def test_purge_docker_error_get_matching_containers( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_not_called() mock_stop_containers.assert_not_called() @@ -174,7 +174,7 @@ def test_purge_docker_error_get_volumes_for_containers( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"]) mock_stop_containers.assert_not_called() @@ -234,7 +234,7 @@ def test_purge_docker_error_get_matching_networks( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"]) mock_stop_containers.assert_called_once_with( @@ -293,7 +293,7 @@ def test_purge_docker_error_stop_containers( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"]) mock_stop_containers.assert_called_once_with( @@ -355,7 +355,7 @@ def test_purge_docker_error_remove_volumes_continues_to_remove_networks( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"]) mock_stop_containers.assert_called_once_with( @@ -425,7 +425,7 @@ def test_purge_docker_error_remove_networks( assert state.get_service_entries(StateTables.STARTED_SERVICES) == [] mock_get_matching_containers.assert_called_once_with( - DEVSERVICES_ORCHESTRATOR_LABEL + [DEVSERVICES_ORCHESTRATOR_LABEL] ) mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"]) mock_stop_containers.assert_called_once_with( diff --git a/tests/utils/test_docker.py b/tests/utils/test_docker.py index c54bfb64..d8bcd74c 100644 --- a/tests/utils/test_docker.py +++ b/tests/utils/test_docker.py @@ -55,7 +55,7 @@ def test_get_matching_containers( ) -> None: mock_check_docker_daemon_running.return_value = None mock_check_output.return_value = "container1\ncontainer2" - matching_containers = get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL) + matching_containers = get_matching_containers([DEVSERVICES_ORCHESTRATOR_LABEL]) mock_check_docker_daemon_running.assert_called_once() mock_check_output.assert_called_once_with( [ @@ -106,7 +106,7 @@ def test_get_matching_containers_docker_daemon_not_running( ) -> None: mock_check_docker_daemon_running.side_effect = DockerDaemonNotRunningError() with pytest.raises(DockerDaemonNotRunningError): - get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL) + get_matching_containers([DEVSERVICES_ORCHESTRATOR_LABEL]) mock_check_docker_daemon_running.assert_called_once() mock_check_output.assert_not_called() @@ -133,7 +133,7 @@ def test_get_matching_containers_error( mock_check_docker_daemon_running.return_value = None mock_check_output.side_effect = subprocess.CalledProcessError(1, "cmd") with pytest.raises(DockerError): - get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL) + get_matching_containers([DEVSERVICES_ORCHESTRATOR_LABEL]) mock_check_docker_daemon_running.assert_called_once() mock_check_output.assert_called_once_with( [ From 070a69c3e243da68081ce38c32c601f3462c84f8 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 23 Jan 2025 23:18:50 -0800 Subject: [PATCH 2/9] initial tests --- tests/commands/test_reset.py | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/commands/test_reset.py diff --git a/tests/commands/test_reset.py b/tests/commands/test_reset.py new file mode 100644 index 00000000..7f8a7c14 --- /dev/null +++ b/tests/commands/test_reset.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from argparse import Namespace +from pathlib import Path +from unittest import mock + +import pytest + +from devservices.commands.reset import reset +from devservices.utils.state import State +from devservices.utils.state import StateTables +from testing.utils import create_config_file + + +@mock.patch("devservices.commands.reset.get_matching_containers", return_value=[]) +def test_reset_no_matching_containers( + mock_get_matching_containers: mock.Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "test-service" + + with pytest.raises(SystemExit): + reset(args) + + captured = capsys.readouterr() + assert "No containers found for test-service" in captured.out + + +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["test-service"] +) +@mock.patch("devservices.commands.reset.get_volumes_for_containers", return_value=[]) +def test_reset_no_matching_volumes( + mock_get_matching_containers: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "test-service" + + with pytest.raises(SystemExit): + reset(args) + + captured = capsys.readouterr() + assert "No volumes found for test-service" in captured.out + + +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["redis"] +) +@mock.patch( + "devservices.commands.reset.get_volumes_for_containers", + return_value=["redis-volume"], +) +@mock.patch("devservices.commands.reset.down") +@mock.patch("devservices.commands.reset.stop_containers") +@mock.patch("devservices.commands.reset.remove_docker_resources") +def test_reset_with_service_name( + mock_remove_docker_resources: mock.Mock, + mock_stop_containers: mock.Mock, + mock_down: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "redis" + service_path = tmp_path / "code" / "test-service" + config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service", + "dependencies": { + "redis": {"description": "Redis"}, + "clickhouse": {"description": "Clickhouse"}, + }, + "modes": {"default": ["redis", "clickhouse"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + "clickhouse": { + "image": "altinity/clickhouse-server:23.8.11.29.altinitystable" + }, + }, + } + create_config_file(service_path, config) + with ( + mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")), + mock.patch( + "devservices.utils.services.get_coderoot", + return_value=str(tmp_path / "code"), + ), + ): + state = State() + state.update_service_entry( + "test-service", "default", StateTables.STARTED_SERVICES + ) + reset(args) + captured = capsys.readouterr() + assert "Resetting docker volumes for redis" in captured.out + assert "Docker volumes have been reset for redis" in captured.out + mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) From 1fdbdd299c0e7f4cee5d7312e8f04e9e31306674 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 24 Jan 2025 09:40:31 -0800 Subject: [PATCH 3/9] add some more tests and have more proper error messaging --- devservices/commands/reset.py | 4 +- devservices/utils/docker.py | 4 +- tests/commands/test_reset.py | 304 ++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 4 deletions(-) diff --git a/devservices/commands/reset.py b/devservices/commands/reset.py index f76dc793..e4a1f071 100644 --- a/devservices/commands/reset.py +++ b/devservices/commands/reset.py @@ -96,7 +96,7 @@ def reset(args: Namespace) -> None: stop_containers(matching_containers, should_remove=True) except DockerError as e: console.failure( - f"Failed to stop and remove {', '.join(matching_containers)} {service_name} {e.stderr}" + f"Failed to stop and remove {', '.join(matching_containers)}\nError: {e.stderr}" ) capture_exception(e) exit(1) @@ -104,7 +104,7 @@ def reset(args: Namespace) -> None: remove_docker_resources("volume", list(matching_volumes)) except DockerError as e: console.failure( - f"Failed to remove volumes {', '.join(matching_volumes)} for {service_name} {e}" + f"Failed to remove volumes {', '.join(matching_volumes)}\nError: {e.stderr}" ) capture_exception(e) exit(1) diff --git a/devservices/utils/docker.py b/devservices/utils/docker.py index 5c52fe68..a56b2423 100644 --- a/devservices/utils/docker.py +++ b/devservices/utils/docker.py @@ -188,7 +188,7 @@ def stop_containers(containers: list[str], should_remove: bool = False) -> None: ["docker", "stop"] + containers, check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ) except subprocess.CalledProcessError as e: raise DockerError( @@ -210,7 +210,7 @@ def remove_docker_resources(resource_type: str, resources: list[str]) -> None: ["docker", resource_type, "rm", *resources], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ) except subprocess.CalledProcessError as e: raise DockerError( diff --git a/tests/commands/test_reset.py b/tests/commands/test_reset.py index 7f8a7c14..2f951585 100644 --- a/tests/commands/test_reset.py +++ b/tests/commands/test_reset.py @@ -7,6 +7,7 @@ import pytest from devservices.commands.reset import reset +from devservices.exceptions import DockerError from devservices.utils.state import State from devservices.utils.state import StateTables from testing.utils import create_config_file @@ -46,6 +47,138 @@ def test_reset_no_matching_volumes( assert "No volumes found for test-service" in captured.out +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["redis"] +) +@mock.patch( + "devservices.commands.reset.get_volumes_for_containers", + return_value=["redis-volume"], +) +@mock.patch("devservices.commands.reset.down") +@mock.patch( + "devservices.commands.reset.stop_containers", + side_effect=DockerError( + command="test-command", returncode=1, stdout="", stderr="test error" + ), +) +@mock.patch("devservices.commands.reset.remove_docker_resources") +def test_reset_with_service_name_container_removal_error( + mock_remove_docker_resources: mock.Mock, + mock_stop_containers: mock.Mock, + mock_down: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "redis" + service_path = tmp_path / "code" / "test-service" + config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service", + "dependencies": { + "redis": {"description": "Redis"}, + "clickhouse": {"description": "Clickhouse"}, + }, + "modes": {"default": ["redis", "clickhouse"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + "clickhouse": { + "image": "altinity/clickhouse-server:23.8.11.29.altinitystable" + }, + }, + } + create_config_file(service_path, config) + with ( + mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")), + mock.patch( + "devservices.utils.services.get_coderoot", + return_value=str(tmp_path / "code"), + ), + ): + state = State() + state.update_service_entry( + "test-service", "default", StateTables.STARTED_SERVICES + ) + with pytest.raises(SystemExit): + reset(args) + captured = capsys.readouterr() + assert "Resetting docker volumes for redis" in captured.out + assert "Bringing down test-service in order to safely reset redis" in captured.out + assert "Failed to stop and remove redis\nError: test error" in captured.out + mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + + +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["redis"] +) +@mock.patch( + "devservices.commands.reset.get_volumes_for_containers", + return_value=["redis-volume"], +) +@mock.patch("devservices.commands.reset.down") +@mock.patch("devservices.commands.reset.stop_containers") +@mock.patch( + "devservices.commands.reset.remove_docker_resources", + side_effect=DockerError( + command="test-command", returncode=1, stdout="", stderr="test error" + ), +) +def test_reset_with_service_name_volume_removal_error( + mock_remove_docker_resources: mock.Mock, + mock_stop_containers: mock.Mock, + mock_down: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "redis" + service_path = tmp_path / "code" / "test-service" + config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service", + "dependencies": { + "redis": {"description": "Redis"}, + "clickhouse": {"description": "Clickhouse"}, + }, + "modes": {"default": ["redis", "clickhouse"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + "clickhouse": { + "image": "altinity/clickhouse-server:23.8.11.29.altinitystable" + }, + }, + } + create_config_file(service_path, config) + with ( + mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")), + mock.patch( + "devservices.utils.services.get_coderoot", + return_value=str(tmp_path / "code"), + ), + ): + state = State() + state.update_service_entry( + "test-service", "default", StateTables.STARTED_SERVICES + ) + with pytest.raises(SystemExit): + reset(args) + captured = capsys.readouterr() + assert "Resetting docker volumes for redis" in captured.out + assert "Bringing down test-service in order to safely reset redis" in captured.out + assert "Failed to remove volumes redis-volume\nError: test error" in captured.out + mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + + @mock.patch( "devservices.commands.reset.get_matching_containers", return_value=["redis"] ) @@ -101,6 +234,177 @@ def test_reset_with_service_name( captured = capsys.readouterr() assert "Resetting docker volumes for redis" in captured.out assert "Docker volumes have been reset for redis" in captured.out + assert "Bringing down test-service in order to safely reset redis" in captured.out mock_down.assert_called_once_with(Namespace(service_name="test-service")) mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) + + +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["redis"] +) +@mock.patch( + "devservices.commands.reset.get_volumes_for_containers", + return_value=["redis-volume"], +) +@mock.patch("devservices.commands.reset.down") +@mock.patch("devservices.commands.reset.stop_containers") +@mock.patch("devservices.commands.reset.remove_docker_resources") +def test_reset_with_multiple_services_depending_on_same_service( + mock_remove_docker_resources: mock.Mock, + mock_stop_containers: mock.Mock, + mock_down: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "redis" + service_1_path = tmp_path / "code" / "test-service-1" + service_1_config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service-1", + "dependencies": { + "redis": {"description": "Redis"}, + "clickhouse": {"description": "Clickhouse"}, + }, + "modes": {"default": ["redis", "clickhouse"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + "clickhouse": { + "image": "altinity/clickhouse-server:23.8.11.29.altinitystable" + }, + }, + } + service_2_path = tmp_path / "code" / "test-service-2" + service_2_config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service-2", + "dependencies": { + "redis": {"description": "Redis"}, + }, + "modes": {"default": ["redis"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + }, + } + create_config_file(service_1_path, service_1_config) + create_config_file(service_2_path, service_2_config) + with ( + mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")), + mock.patch( + "devservices.utils.services.get_coderoot", + return_value=str(tmp_path / "code"), + ), + ): + state = State() + state.update_service_entry( + "test-service-1", "default", StateTables.STARTED_SERVICES + ) + state.update_service_entry( + "test-service-2", "default", StateTables.STARTED_SERVICES + ) + reset(args) + captured = capsys.readouterr() + assert "Resetting docker volumes for redis" in captured.out + assert "Docker volumes have been reset for redis" in captured.out + assert "Bringing down test-service-1 in order to safely reset redis" in captured.out + assert "Bringing down test-service-2 in order to safely reset redis" in captured.out + mock_down.assert_has_calls( + [ + mock.call(Namespace(service_name="test-service-1")), + mock.call(Namespace(service_name="test-service-2")), + ], + any_order=True, + ) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) + + +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["clickhouse"] +) +@mock.patch( + "devservices.commands.reset.get_volumes_for_containers", + return_value=["clickhouse-volume"], +) +@mock.patch("devservices.commands.reset.down") +@mock.patch("devservices.commands.reset.stop_containers") +@mock.patch("devservices.commands.reset.remove_docker_resources") +def test_reset_with_multiple_services_depending_on_different_service( + mock_remove_docker_resources: mock.Mock, + mock_stop_containers: mock.Mock, + mock_down: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "clickhouse" + service_1_path = tmp_path / "code" / "test-service-1" + service_1_config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service-1", + "dependencies": { + "redis": {"description": "Redis"}, + "clickhouse": {"description": "Clickhouse"}, + }, + "modes": {"default": ["redis", "clickhouse"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + "clickhouse": { + "image": "altinity/clickhouse-server:23.8.11.29.altinitystable" + }, + }, + } + service_2_path = tmp_path / "code" / "test-service-2" + service_2_config = { + "x-sentry-service-config": { + "version": 0.1, + "service_name": "test-service-2", + "dependencies": { + "redis": {"description": "Redis"}, + }, + "modes": {"default": ["redis"]}, + }, + "services": { + "redis": {"image": "redis:6.2.14-alpine"}, + }, + } + create_config_file(service_1_path, service_1_config) + create_config_file(service_2_path, service_2_config) + with ( + mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")), + mock.patch( + "devservices.utils.services.get_coderoot", + return_value=str(tmp_path / "code"), + ), + ): + state = State() + state.update_service_entry( + "test-service-1", "default", StateTables.STARTED_SERVICES + ) + state.update_service_entry( + "test-service-2", "default", StateTables.STARTED_SERVICES + ) + reset(args) + captured = capsys.readouterr() + assert "Resetting docker volumes for clickhouse" in captured.out + assert "Docker volumes have been reset for clickhouse" in captured.out + assert ( + "Bringing down test-service-1 in order to safely reset clickhouse" + in captured.out + ) + mock_down.assert_called_once_with(Namespace(service_name="test-service-1")) + mock_stop_containers.assert_called_once_with(["clickhouse"], should_remove=True) + mock_remove_docker_resources.assert_called_once_with( + "volume", ["clickhouse-volume"] + ) From 38af2d43b9d92827be52ff5ffb50770cb1781a1f Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 24 Jan 2025 09:44:22 -0800 Subject: [PATCH 4/9] update docker tests --- tests/utils/test_docker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/utils/test_docker.py b/tests/utils/test_docker.py index d8bcd74c..a2ac4802 100644 --- a/tests/utils/test_docker.py +++ b/tests/utils/test_docker.py @@ -235,7 +235,7 @@ def test_stop_containers_should_not_remove( ["docker", "stop", *containers], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ) @@ -259,13 +259,13 @@ def test_stop_containers_should_remove( ["docker", "stop", *containers], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ), mock.call( ["docker", "container", "rm", *containers], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ), ] ) @@ -283,7 +283,7 @@ def test_stop_containers_stop_error( ["docker", "stop", *containers], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ) @@ -301,13 +301,13 @@ def test_stop_containers_remove_error( ["docker", "stop", *containers], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ), mock.call( ["docker", "container", "rm", *containers], check=True, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, ), ] ) From 7844216c937349f284930900324041c5b0f256bc Mon Sep 17 00:00:00 2001 From: Ian Woodard <17186604+IanWoodard@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:44:57 -0800 Subject: [PATCH 5/9] Adding more test coverage --- tests/commands/test_reset.py | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/commands/test_reset.py b/tests/commands/test_reset.py index 2f951585..31a97407 100644 --- a/tests/commands/test_reset.py +++ b/tests/commands/test_reset.py @@ -7,12 +7,53 @@ import pytest from devservices.commands.reset import reset +from devservices.exceptions import DockerDaemonNotRunningError from devservices.exceptions import DockerError from devservices.utils.state import State from devservices.utils.state import StateTables from testing.utils import create_config_file +@mock.patch( + "devservices.commands.reset.get_matching_containers", + side_effect=DockerDaemonNotRunningError(), +) +def test_reset_docker_daemon_not_running( + mock_get_matching_containers: mock.Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "test-service" + + reset(args) + + captured = capsys.readouterr() + assert ( + "Unable to connect to the docker daemon. Is the docker daemon running?" + in captured.out.strip() + ) + + +@mock.patch( + "devservices.commands.reset.get_matching_containers", + side_effect=DockerError( + command="test-command", returncode=1, stdout="", stderr="test error" + ), +) +def test_reset_failed_to_get_matching_containers( + mock_get_matching_containers: mock.Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "test-service" + + with pytest.raises(SystemExit): + reset(args) + + captured = capsys.readouterr() + assert "Failed to get matching containers" in captured.out + + @mock.patch("devservices.commands.reset.get_matching_containers", return_value=[]) def test_reset_no_matching_containers( mock_get_matching_containers: mock.Mock, @@ -47,6 +88,30 @@ def test_reset_no_matching_volumes( assert "No volumes found for test-service" in captured.out +@mock.patch( + "devservices.commands.reset.get_matching_containers", return_value=["test-service"] +) +@mock.patch( + "devservices.commands.reset.get_volumes_for_containers", + side_effect=DockerError( + command="test-command", returncode=1, stdout="", stderr="test error" + ), +) +def test_reset_failed_to_get_matching_volumes( + mock_get_matching_containers: mock.Mock, + mock_get_volumes_for_containers: mock.Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + args = Namespace() + args.service_name = "test-service" + + with pytest.raises(SystemExit): + reset(args) + + captured = capsys.readouterr() + assert "Failed to get matching volumes" in captured.out + + @mock.patch( "devservices.commands.reset.get_matching_containers", return_value=["redis"] ) From 91ff2ee2c2648c43fe8aff9a8dae861dbd5e84fb Mon Sep 17 00:00:00 2001 From: Ian Woodard <17186604+IanWoodard@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:27:48 -0800 Subject: [PATCH 6/9] Updating tests --- tests/commands/test_reset.py | 93 ++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/tests/commands/test_reset.py b/tests/commands/test_reset.py index 31a97407..1b8b2f8e 100644 --- a/tests/commands/test_reset.py +++ b/tests/commands/test_reset.py @@ -7,6 +7,7 @@ import pytest from devservices.commands.reset import reset +from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL from devservices.exceptions import DockerDaemonNotRunningError from devservices.exceptions import DockerError from devservices.utils.state import State @@ -27,6 +28,10 @@ def test_reset_docker_daemon_not_running( reset(args) + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=test-service"] + ) + captured = capsys.readouterr() assert ( "Unable to connect to the docker daemon. Is the docker daemon running?" @@ -50,6 +55,10 @@ def test_reset_failed_to_get_matching_containers( with pytest.raises(SystemExit): reset(args) + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=test-service"] + ) + captured = capsys.readouterr() assert "Failed to get matching containers" in captured.out @@ -65,6 +74,10 @@ def test_reset_no_matching_containers( with pytest.raises(SystemExit): reset(args) + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=test-service"] + ) + captured = capsys.readouterr() assert "No containers found for test-service" in captured.out @@ -74,8 +87,8 @@ def test_reset_no_matching_containers( ) @mock.patch("devservices.commands.reset.get_volumes_for_containers", return_value=[]) def test_reset_no_matching_volumes( - mock_get_matching_containers: mock.Mock, mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, capsys: pytest.CaptureFixture[str], ) -> None: args = Namespace() @@ -84,6 +97,11 @@ def test_reset_no_matching_volumes( with pytest.raises(SystemExit): reset(args) + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=test-service"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["test-service"]) + captured = capsys.readouterr() assert "No volumes found for test-service" in captured.out @@ -98,8 +116,8 @@ def test_reset_no_matching_volumes( ), ) def test_reset_failed_to_get_matching_volumes( - mock_get_matching_containers: mock.Mock, mock_get_volumes_for_containers: mock.Mock, + mock_get_matching_containers: mock.Mock, capsys: pytest.CaptureFixture[str], ) -> None: args = Namespace() @@ -108,6 +126,11 @@ def test_reset_failed_to_get_matching_volumes( with pytest.raises(SystemExit): reset(args) + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=test-service"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["test-service"]) + captured = capsys.readouterr() assert "Failed to get matching volumes" in captured.out @@ -170,12 +193,19 @@ def test_reset_with_service_name_container_removal_error( ) with pytest.raises(SystemExit): reset(args) + + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["redis"]) + mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + mock_remove_docker_resources.assert_not_called() + captured = capsys.readouterr() assert "Resetting docker volumes for redis" in captured.out assert "Bringing down test-service in order to safely reset redis" in captured.out assert "Failed to stop and remove redis\nError: test error" in captured.out - mock_down.assert_called_once_with(Namespace(service_name="test-service")) - mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) @mock.patch( @@ -236,12 +266,19 @@ def test_reset_with_service_name_volume_removal_error( ) with pytest.raises(SystemExit): reset(args) + + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["redis"]) + mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) + captured = capsys.readouterr() assert "Resetting docker volumes for redis" in captured.out assert "Bringing down test-service in order to safely reset redis" in captured.out assert "Failed to remove volumes redis-volume\nError: test error" in captured.out - mock_down.assert_called_once_with(Namespace(service_name="test-service")) - mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) @mock.patch( @@ -296,13 +333,19 @@ def test_reset_with_service_name( "test-service", "default", StateTables.STARTED_SERVICES ) reset(args) + + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["redis"]) + mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) + mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) + captured = capsys.readouterr() assert "Resetting docker volumes for redis" in captured.out assert "Docker volumes have been reset for redis" in captured.out assert "Bringing down test-service in order to safely reset redis" in captured.out - mock_down.assert_called_once_with(Namespace(service_name="test-service")) - mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) - mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) @mock.patch( @@ -375,11 +418,11 @@ def test_reset_with_multiple_services_depending_on_same_service( "test-service-2", "default", StateTables.STARTED_SERVICES ) reset(args) - captured = capsys.readouterr() - assert "Resetting docker volumes for redis" in captured.out - assert "Docker volumes have been reset for redis" in captured.out - assert "Bringing down test-service-1 in order to safely reset redis" in captured.out - assert "Bringing down test-service-2 in order to safely reset redis" in captured.out + + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["redis"]) mock_down.assert_has_calls( [ mock.call(Namespace(service_name="test-service-1")), @@ -390,6 +433,12 @@ def test_reset_with_multiple_services_depending_on_same_service( mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) + captured = capsys.readouterr() + assert "Resetting docker volumes for redis" in captured.out + assert "Docker volumes have been reset for redis" in captured.out + assert "Bringing down test-service-1 in order to safely reset redis" in captured.out + assert "Bringing down test-service-2 in order to safely reset redis" in captured.out + @mock.patch( "devservices.commands.reset.get_matching_containers", return_value=["clickhouse"] @@ -461,6 +510,17 @@ def test_reset_with_multiple_services_depending_on_different_service( "test-service-2", "default", StateTables.STARTED_SERVICES ) reset(args) + + mock_get_matching_containers.assert_called_once_with( + [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=clickhouse"] + ) + mock_get_volumes_for_containers.assert_called_once_with(["clickhouse"]) + mock_down.assert_called_once_with(Namespace(service_name="test-service-1")) + mock_stop_containers.assert_called_once_with(["clickhouse"], should_remove=True) + mock_remove_docker_resources.assert_called_once_with( + "volume", ["clickhouse-volume"] + ) + captured = capsys.readouterr() assert "Resetting docker volumes for clickhouse" in captured.out assert "Docker volumes have been reset for clickhouse" in captured.out @@ -468,8 +528,3 @@ def test_reset_with_multiple_services_depending_on_different_service( "Bringing down test-service-1 in order to safely reset clickhouse" in captured.out ) - mock_down.assert_called_once_with(Namespace(service_name="test-service-1")) - mock_stop_containers.assert_called_once_with(["clickhouse"], should_remove=True) - mock_remove_docker_resources.assert_called_once_with( - "volume", ["clickhouse-volume"] - ) From 90883b4ca80cae1b317b8f8316838bd00ef88e30 Mon Sep 17 00:00:00 2001 From: Ian Woodard <17186604+IanWoodard@users.noreply.github.com> Date: Fri, 2 May 2025 10:21:30 -0700 Subject: [PATCH 7/9] fixing dependency graph logic --- devservices/commands/reset.py | 7 ++++++- tests/commands/test_reset.py | 21 +++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/devservices/commands/reset.py b/devservices/commands/reset.py index e4a1f071..43a575c0 100644 --- a/devservices/commands/reset.py +++ b/devservices/commands/reset.py @@ -13,6 +13,8 @@ from devservices.utils.console import Console from devservices.utils.console import Status from devservices.utils.dependencies import construct_dependency_graph +from devservices.utils.dependencies import DependencyNode +from devservices.utils.dependencies import DependencyType from devservices.utils.docker import get_matching_containers from devservices.utils.docker import get_volumes_for_containers from devservices.utils.docker import remove_docker_resources @@ -82,7 +84,10 @@ def reset(args: Namespace) -> None: ) active_modes = starting_active_modes or started_active_modes dependency_graph = construct_dependency_graph(active_service, active_modes) - if service_name in dependency_graph.graph: + if ( + DependencyNode(name=service_name, dependency_type=DependencyType.COMPOSE) + in dependency_graph.graph + ): console.warning( f"Bringing down {active_service_name} in order to safely reset {service_name}" ) diff --git a/tests/commands/test_reset.py b/tests/commands/test_reset.py index 1b8b2f8e..348e1587 100644 --- a/tests/commands/test_reset.py +++ b/tests/commands/test_reset.py @@ -23,8 +23,7 @@ def test_reset_docker_daemon_not_running( mock_get_matching_containers: mock.Mock, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "test-service" + args = Namespace(service_name="test-service") reset(args) @@ -91,8 +90,7 @@ def test_reset_no_matching_volumes( mock_get_matching_containers: mock.Mock, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "test-service" + args = Namespace(service_name="test-service") with pytest.raises(SystemExit): reset(args) @@ -159,8 +157,7 @@ def test_reset_with_service_name_container_removal_error( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "redis" + args = Namespace(service_name="redis") service_path = tmp_path / "code" / "test-service" config = { "x-sentry-service-config": { @@ -232,8 +229,7 @@ def test_reset_with_service_name_volume_removal_error( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "redis" + args = Namespace(service_name="redis") service_path = tmp_path / "code" / "test-service" config = { "x-sentry-service-config": { @@ -300,8 +296,7 @@ def test_reset_with_service_name( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "redis" + args = Namespace(service_name="redis") service_path = tmp_path / "code" / "test-service" config = { "x-sentry-service-config": { @@ -367,8 +362,7 @@ def test_reset_with_multiple_services_depending_on_same_service( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "redis" + args = Namespace(service_name="redis") service_1_path = tmp_path / "code" / "test-service-1" service_1_config = { "x-sentry-service-config": { @@ -459,8 +453,7 @@ def test_reset_with_multiple_services_depending_on_different_service( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: - args = Namespace() - args.service_name = "clickhouse" + args = Namespace(service_name="clickhouse") service_1_path = tmp_path / "code" / "test-service-1" service_1_config = { "x-sentry-service-config": { From 3b6e24308c3775c50017cccbd07540ef9889424a Mon Sep 17 00:00:00 2001 From: Ian Woodard <17186604+IanWoodard@users.noreply.github.com> Date: Fri, 2 May 2025 10:22:44 -0700 Subject: [PATCH 8/9] change question to statement --- devservices/commands/reset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devservices/commands/reset.py b/devservices/commands/reset.py index 43a575c0..7cb75c14 100644 --- a/devservices/commands/reset.py +++ b/devservices/commands/reset.py @@ -73,7 +73,7 @@ def reset(args: Namespace) -> None: starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES)) active_service_names = starting_services.union(started_services) - # TODO: Should we add threading here to speed up the process? + # TODO: We should add threading here to speed up the process for active_service_name in active_service_names: active_service = find_matching_service(active_service_name) starting_active_modes = state.get_active_modes_for_service( From a588055116a4b9dacd8fd27cc645ab21e19e95df Mon Sep 17 00:00:00 2001 From: Ian Woodard <17186604+IanWoodard@users.noreply.github.com> Date: Fri, 2 May 2025 14:07:52 -0700 Subject: [PATCH 9/9] Fixing down usage --- devservices/commands/reset.py | 2 +- tests/commands/test_reset.py | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/devservices/commands/reset.py b/devservices/commands/reset.py index 7cb75c14..1e936c5f 100644 --- a/devservices/commands/reset.py +++ b/devservices/commands/reset.py @@ -91,7 +91,7 @@ def reset(args: Namespace) -> None: console.warning( f"Bringing down {active_service_name} in order to safely reset {service_name}" ) - down(Namespace(service_name=active_service_name)) + down(Namespace(service_name=active_service_name, exclude_local=True)) with Status( lambda: console.warning(f"Resetting docker volumes for {service_name}"), diff --git a/tests/commands/test_reset.py b/tests/commands/test_reset.py index 348e1587..e92c2664 100644 --- a/tests/commands/test_reset.py +++ b/tests/commands/test_reset.py @@ -195,7 +195,9 @@ def test_reset_with_service_name_container_removal_error( [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] ) mock_get_volumes_for_containers.assert_called_once_with(["redis"]) - mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_down.assert_called_once_with( + Namespace(service_name="test-service", exclude_local=True) + ) mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) mock_remove_docker_resources.assert_not_called() @@ -267,7 +269,9 @@ def test_reset_with_service_name_volume_removal_error( [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] ) mock_get_volumes_for_containers.assert_called_once_with(["redis"]) - mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_down.assert_called_once_with( + Namespace(service_name="test-service", exclude_local=True) + ) mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) @@ -333,7 +337,9 @@ def test_reset_with_service_name( [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=redis"] ) mock_get_volumes_for_containers.assert_called_once_with(["redis"]) - mock_down.assert_called_once_with(Namespace(service_name="test-service")) + mock_down.assert_called_once_with( + Namespace(service_name="test-service", exclude_local=True) + ) mock_stop_containers.assert_called_once_with(["redis"], should_remove=True) mock_remove_docker_resources.assert_called_once_with("volume", ["redis-volume"]) @@ -419,8 +425,8 @@ def test_reset_with_multiple_services_depending_on_same_service( mock_get_volumes_for_containers.assert_called_once_with(["redis"]) mock_down.assert_has_calls( [ - mock.call(Namespace(service_name="test-service-1")), - mock.call(Namespace(service_name="test-service-2")), + mock.call(Namespace(service_name="test-service-1", exclude_local=True)), + mock.call(Namespace(service_name="test-service-2", exclude_local=True)), ], any_order=True, ) @@ -508,7 +514,9 @@ def test_reset_with_multiple_services_depending_on_different_service( [DEVSERVICES_ORCHESTRATOR_LABEL, "com.docker.compose.service=clickhouse"] ) mock_get_volumes_for_containers.assert_called_once_with(["clickhouse"]) - mock_down.assert_called_once_with(Namespace(service_name="test-service-1")) + mock_down.assert_called_once_with( + Namespace(service_name="test-service-1", exclude_local=True) + ) mock_stop_containers.assert_called_once_with(["clickhouse"], should_remove=True) mock_remove_docker_resources.assert_called_once_with( "volume", ["clickhouse-volume"]