Skip to content

Commit 6e7d09b

Browse files
committed
feat(reset): Adding reset command
1 parent bb7c61d commit 6e7d09b

File tree

6 files changed

+132
-16
lines changed

6 files changed

+132
-16
lines changed

devservices/commands/purge.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ def purge(_args: Namespace) -> None:
4040
state.clear_state()
4141

4242
try:
43-
devservices_containers = get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
43+
devservices_containers = get_matching_containers(
44+
[DEVSERVICES_ORCHESTRATOR_LABEL]
45+
)
4446
except DockerDaemonNotRunningError as e:
4547
console.warning(str(e))
4648
return

devservices/commands/reset.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
from argparse import _SubParsersAction
4+
from argparse import ArgumentParser
5+
from argparse import Namespace
6+
7+
from sentry_sdk import capture_exception
8+
9+
from devservices.commands.down import down
10+
from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
11+
from devservices.exceptions import DockerDaemonNotRunningError
12+
from devservices.exceptions import DockerError
13+
from devservices.utils.console import Console
14+
from devservices.utils.console import Status
15+
from devservices.utils.dependencies import construct_dependency_graph
16+
from devservices.utils.docker import get_matching_containers
17+
from devservices.utils.docker import get_volumes_for_containers
18+
from devservices.utils.docker import remove_docker_resources
19+
from devservices.utils.docker import stop_containers
20+
from devservices.utils.services import find_matching_service
21+
from devservices.utils.state import State
22+
from devservices.utils.state import StateTables
23+
24+
25+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
26+
parser = subparsers.add_parser("reset", help="Reset a service's volumes")
27+
parser.add_argument(
28+
"service_name",
29+
help="Name of the service to reset volumes for",
30+
nargs="?",
31+
default=None,
32+
)
33+
parser.set_defaults(func=reset)
34+
35+
36+
def reset(args: Namespace) -> None:
37+
"""Reset a specified service's volumes."""
38+
console = Console()
39+
service_name = args.service_name
40+
41+
try:
42+
matching_containers = get_matching_containers(
43+
[
44+
DEVSERVICES_ORCHESTRATOR_LABEL,
45+
f"com.docker.compose.service={args.service_name}",
46+
]
47+
)
48+
except DockerDaemonNotRunningError as e:
49+
console.warning(str(e))
50+
return
51+
except DockerError as e:
52+
console.failure(f"Failed to get matching containers {e.stderr}")
53+
exit(1)
54+
55+
if len(matching_containers) == 0:
56+
console.failure(f"No containers found for {service_name}")
57+
exit(1)
58+
59+
try:
60+
matching_volumes = get_volumes_for_containers(matching_containers)
61+
except DockerError as e:
62+
console.failure(f"Failed to get matching volumes {e.stderr}")
63+
exit(1)
64+
65+
if len(matching_volumes) == 0:
66+
console.failure(f"No volumes found for {service_name}")
67+
exit(1)
68+
69+
state = State()
70+
started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
71+
starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
72+
active_service_names = starting_services.union(started_services)
73+
74+
# TODO: Should we add threading here to speed up the process?
75+
for active_service_name in active_service_names:
76+
active_service = find_matching_service(active_service_name)
77+
starting_active_modes = state.get_active_modes_for_service(
78+
active_service_name, StateTables.STARTING_SERVICES
79+
)
80+
started_active_modes = state.get_active_modes_for_service(
81+
active_service_name, StateTables.STARTED_SERVICES
82+
)
83+
active_modes = starting_active_modes or started_active_modes
84+
dependency_graph = construct_dependency_graph(active_service, active_modes)
85+
if service_name in dependency_graph.graph:
86+
console.warning(
87+
f"Bringing down {active_service_name} in order to safely reset {service_name}"
88+
)
89+
down(Namespace(service_name=active_service_name))
90+
91+
with Status(
92+
lambda: console.warning(f"Resetting docker volumes for {service_name}"),
93+
lambda: console.success(f"Docker volumes have been reset for {service_name}"),
94+
):
95+
try:
96+
stop_containers(matching_containers, should_remove=True)
97+
except DockerError as e:
98+
console.failure(
99+
f"Failed to stop and remove {', '.join(matching_containers)} {service_name} {e.stderr}"
100+
)
101+
capture_exception(e)
102+
exit(1)
103+
try:
104+
remove_docker_resources("volume", list(matching_volumes))
105+
except DockerError as e:
106+
console.failure(
107+
f"Failed to remove volumes {', '.join(matching_volumes)} for {service_name} {e}"
108+
)
109+
capture_exception(e)
110+
exit(1)

devservices/main.py

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from devservices.commands import list_services
2121
from devservices.commands import logs
2222
from devservices.commands import purge
23+
from devservices.commands import reset
2324
from devservices.commands import status
2425
from devservices.commands import up
2526
from devservices.commands import update
@@ -87,6 +88,7 @@ def main() -> None:
8788
logs.add_parser(subparsers)
8889
update.add_parser(subparsers)
8990
purge.add_parser(subparsers)
91+
reset.add_parser(subparsers)
9092

9193
args = parser.parse_args()
9294

devservices/utils/docker.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,14 @@ def wait_for_healthy(container_name: str, status: Status) -> None:
7878
raise ContainerHealthcheckFailedError(container_name, HEALTHCHECK_TIMEOUT)
7979

8080

81-
def get_matching_containers(label: str) -> list[str]:
81+
def get_matching_containers(labels: list[str]) -> list[str]:
8282
"""
8383
Returns a list of container names with the given label
8484
"""
8585
check_docker_daemon_running()
86+
filters = []
87+
for label in labels:
88+
filters.extend(["--filter", f"label={label}"])
8689
try:
8790
return (
8891
subprocess.check_output(
@@ -91,9 +94,8 @@ def get_matching_containers(label: str) -> list[str]:
9194
"ps",
9295
"-a",
9396
"-q",
94-
"--filter",
95-
f"label={label}",
96-
],
97+
]
98+
+ filters,
9799
text=True,
98100
stderr=subprocess.DEVNULL,
99101
)
@@ -102,7 +104,7 @@ def get_matching_containers(label: str) -> list[str]:
102104
)
103105
except subprocess.CalledProcessError as e:
104106
raise DockerError(
105-
command=f"docker ps -q --filter label={label}",
107+
command=f"docker ps -a -q {' '.join(filters)}",
106108
returncode=e.returncode,
107109
stdout=e.stdout,
108110
stderr=e.stderr,

tests/commands/test_purge.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_purge_docker_daemon_not_running(
5858
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
5959

6060
mock_get_matching_containers.assert_called_once_with(
61-
DEVSERVICES_ORCHESTRATOR_LABEL
61+
[DEVSERVICES_ORCHESTRATOR_LABEL]
6262
)
6363
mock_get_volumes_for_containers.assert_not_called()
6464
mock_stop_containers.assert_not_called()
@@ -117,7 +117,7 @@ def test_purge_docker_error_get_matching_containers(
117117
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
118118

119119
mock_get_matching_containers.assert_called_once_with(
120-
DEVSERVICES_ORCHESTRATOR_LABEL
120+
[DEVSERVICES_ORCHESTRATOR_LABEL]
121121
)
122122
mock_get_volumes_for_containers.assert_not_called()
123123
mock_stop_containers.assert_not_called()
@@ -174,7 +174,7 @@ def test_purge_docker_error_get_volumes_for_containers(
174174
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
175175

176176
mock_get_matching_containers.assert_called_once_with(
177-
DEVSERVICES_ORCHESTRATOR_LABEL
177+
[DEVSERVICES_ORCHESTRATOR_LABEL]
178178
)
179179
mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"])
180180
mock_stop_containers.assert_not_called()
@@ -234,7 +234,7 @@ def test_purge_docker_error_get_matching_networks(
234234
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
235235

236236
mock_get_matching_containers.assert_called_once_with(
237-
DEVSERVICES_ORCHESTRATOR_LABEL
237+
[DEVSERVICES_ORCHESTRATOR_LABEL]
238238
)
239239
mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"])
240240
mock_stop_containers.assert_called_once_with(
@@ -293,7 +293,7 @@ def test_purge_docker_error_stop_containers(
293293
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
294294

295295
mock_get_matching_containers.assert_called_once_with(
296-
DEVSERVICES_ORCHESTRATOR_LABEL
296+
[DEVSERVICES_ORCHESTRATOR_LABEL]
297297
)
298298
mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"])
299299
mock_stop_containers.assert_called_once_with(
@@ -355,7 +355,7 @@ def test_purge_docker_error_remove_volumes_continues_to_remove_networks(
355355
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
356356

357357
mock_get_matching_containers.assert_called_once_with(
358-
DEVSERVICES_ORCHESTRATOR_LABEL
358+
[DEVSERVICES_ORCHESTRATOR_LABEL]
359359
)
360360
mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"])
361361
mock_stop_containers.assert_called_once_with(
@@ -425,7 +425,7 @@ def test_purge_docker_error_remove_networks(
425425
assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
426426

427427
mock_get_matching_containers.assert_called_once_with(
428-
DEVSERVICES_ORCHESTRATOR_LABEL
428+
[DEVSERVICES_ORCHESTRATOR_LABEL]
429429
)
430430
mock_get_volumes_for_containers.assert_called_once_with(["abc", "def", "ghi"])
431431
mock_stop_containers.assert_called_once_with(

tests/utils/test_docker.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_get_matching_containers(
5555
) -> None:
5656
mock_check_docker_daemon_running.return_value = None
5757
mock_check_output.return_value = "container1\ncontainer2"
58-
matching_containers = get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
58+
matching_containers = get_matching_containers([DEVSERVICES_ORCHESTRATOR_LABEL])
5959
mock_check_docker_daemon_running.assert_called_once()
6060
mock_check_output.assert_called_once_with(
6161
[
@@ -106,7 +106,7 @@ def test_get_matching_containers_docker_daemon_not_running(
106106
) -> None:
107107
mock_check_docker_daemon_running.side_effect = DockerDaemonNotRunningError()
108108
with pytest.raises(DockerDaemonNotRunningError):
109-
get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
109+
get_matching_containers([DEVSERVICES_ORCHESTRATOR_LABEL])
110110
mock_check_docker_daemon_running.assert_called_once()
111111
mock_check_output.assert_not_called()
112112

@@ -133,7 +133,7 @@ def test_get_matching_containers_error(
133133
mock_check_docker_daemon_running.return_value = None
134134
mock_check_output.side_effect = subprocess.CalledProcessError(1, "cmd")
135135
with pytest.raises(DockerError):
136-
get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
136+
get_matching_containers([DEVSERVICES_ORCHESTRATOR_LABEL])
137137
mock_check_docker_daemon_running.assert_called_once()
138138
mock_check_output.assert_called_once_with(
139139
[

0 commit comments

Comments
 (0)