Skip to content
4 changes: 3 additions & 1 deletion devservices/commands/purge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions devservices/commands/reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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.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
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(

Check warning on line 29 in devservices/commands/reset.py

View check run for this annotation

Codecov / codecov/patch

devservices/commands/reset.py#L28-L29

Added lines #L28 - L29 were not covered by tests
"service_name",
help="Name of the service to reset volumes for",
nargs="?",
default=None,
)
parser.set_defaults(func=reset)

Check warning on line 35 in devservices/commands/reset.py

View check run for this annotation

Codecov / codecov/patch

devservices/commands/reset.py#L35

Added line #L35 was not covered by tests


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: 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(
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 (
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}"
)
down(Namespace(service_name=active_service_name, exclude_local=True))

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)}\nError: {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)}\nError: {e.stderr}"
)
capture_exception(e)
exit(1)
2 changes: 2 additions & 0 deletions devservices/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 serve
from devservices.commands import status
from devservices.commands import toggle
Expand Down Expand Up @@ -150,6 +151,7 @@ def main() -> None:
purge.add_parser(subparsers)
serve.add_parser(subparsers)
toggle.add_parser(subparsers)
reset.add_parser(subparsers)

args = parser.parse_args()

Expand Down
16 changes: 9 additions & 7 deletions devservices/utils/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ def wait_for_healthy(container: ContainerNames, status: Status) -> None:
raise ContainerHealthcheckFailedError(container.short_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(
Expand All @@ -101,9 +104,8 @@ def get_matching_containers(label: str) -> list[str]:
"ps",
"-a",
"-q",
"--filter",
f"label={label}",
],
]
+ filters,
text=True,
stderr=subprocess.DEVNULL,
)
Expand All @@ -112,7 +114,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,
Expand Down Expand Up @@ -196,7 +198,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(
Expand All @@ -218,7 +220,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(
Expand Down
14 changes: 7 additions & 7 deletions tests/commands/test_purge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading