devservices 1.0.5__tar.gz → 1.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devservices-1.0.5 → devservices-1.0.6}/PKG-INFO +1 -1
- {devservices-1.0.5 → devservices-1.0.6}/README.md +1 -1
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/down.py +8 -3
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/purge.py +10 -13
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/update.py +1 -1
- {devservices-1.0.5 → devservices-1.0.6}/devservices/constants.py +11 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/exceptions.py +8 -2
- {devservices-1.0.5 → devservices-1.0.6}/devservices/main.py +2 -2
- devservices-1.0.6/devservices/utils/check_for_update.py +59 -0
- devservices-1.0.6/devservices/utils/docker.py +88 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/SOURCES.txt +2 -1
- {devservices-1.0.5 → devservices-1.0.6}/pyproject.toml +1 -1
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_purge.py +98 -51
- devservices-1.0.6/tests/utils/test_check_for_update.py +170 -0
- devservices-1.0.6/tests/utils/test_docker.py +182 -0
- devservices-1.0.5/devservices/commands/check_for_update.py +0 -14
- devservices-1.0.5/devservices/utils/docker.py +0 -36
- devservices-1.0.5/tests/utils/test_docker.py +0 -47
- {devservices-1.0.5 → devservices-1.0.6}/LICENSE.md +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/list_services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/logs.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/status.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/commands/up.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/console.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/dependencies.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/devenv.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/docker_compose.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices/utils/state.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/setup.cfg +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/testing/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/testing/utils.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_down.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_list_services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_logs.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_status.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_up.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/commands/test_update.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/conftest.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_dependencies.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.6}/tests/utils/test_state.py +0 -0
|
@@ -90,9 +90,14 @@ def down(args: Namespace) -> None:
|
|
|
90
90
|
capture_exception(de)
|
|
91
91
|
status.failure(str(de))
|
|
92
92
|
exit(1)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
try:
|
|
94
|
+
remote_dependencies = get_non_shared_remote_dependencies(
|
|
95
|
+
service, remote_dependencies
|
|
96
|
+
)
|
|
97
|
+
except DependencyError as de:
|
|
98
|
+
capture_exception(de)
|
|
99
|
+
status.failure(str(de))
|
|
100
|
+
exit(1)
|
|
96
101
|
try:
|
|
97
102
|
_down(service, remote_dependencies, list(mode_dependencies), status)
|
|
98
103
|
except DockerComposeError as dce:
|
|
@@ -8,11 +8,13 @@ from argparse import ArgumentParser
|
|
|
8
8
|
from argparse import Namespace
|
|
9
9
|
|
|
10
10
|
from devservices.constants import DEVSERVICES_CACHE_DIR
|
|
11
|
+
from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
|
|
11
12
|
from devservices.constants import DOCKER_NETWORK_NAME
|
|
12
13
|
from devservices.exceptions import DockerDaemonNotRunningError
|
|
14
|
+
from devservices.exceptions import DockerError
|
|
13
15
|
from devservices.utils.console import Console
|
|
14
16
|
from devservices.utils.console import Status
|
|
15
|
-
from devservices.utils.docker import
|
|
17
|
+
from devservices.utils.docker import stop_matching_containers
|
|
16
18
|
from devservices.utils.state import State
|
|
17
19
|
|
|
18
20
|
|
|
@@ -25,14 +27,6 @@ def purge(_args: Namespace) -> None:
|
|
|
25
27
|
"""Purge the local devservices cache."""
|
|
26
28
|
console = Console()
|
|
27
29
|
|
|
28
|
-
# Prompt the user to stop all running containers
|
|
29
|
-
should_stop_containers = console.confirm(
|
|
30
|
-
"Warning: Purging stops all running containers and clears devservices state. Would you like to continue?"
|
|
31
|
-
)
|
|
32
|
-
if not should_stop_containers:
|
|
33
|
-
console.warning("Purge canceled")
|
|
34
|
-
return
|
|
35
|
-
|
|
36
30
|
if os.path.exists(DEVSERVICES_CACHE_DIR):
|
|
37
31
|
try:
|
|
38
32
|
shutil.rmtree(DEVSERVICES_CACHE_DIR)
|
|
@@ -42,13 +36,16 @@ def purge(_args: Namespace) -> None:
|
|
|
42
36
|
state = State()
|
|
43
37
|
state.clear_state()
|
|
44
38
|
with Status(
|
|
45
|
-
lambda: console.warning("Stopping all running containers"),
|
|
46
|
-
lambda: console.success("All running containers have been stopped"),
|
|
39
|
+
lambda: console.warning("Stopping all running devservices containers"),
|
|
40
|
+
lambda: console.success("All running devservices containers have been stopped"),
|
|
47
41
|
):
|
|
48
42
|
try:
|
|
49
|
-
|
|
43
|
+
stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
|
|
50
44
|
except DockerDaemonNotRunningError:
|
|
51
|
-
console.warning("The docker daemon not running, no containers to stop")
|
|
45
|
+
console.warning("The docker daemon is not running, no containers to stop")
|
|
46
|
+
except DockerError as e:
|
|
47
|
+
console.failure(f"Failed to stop running devservices containers {e.stderr}")
|
|
48
|
+
exit(1)
|
|
52
49
|
|
|
53
50
|
console.warning("Removing any devservices networks")
|
|
54
51
|
devservices_networks = (
|
|
@@ -7,10 +7,10 @@ from argparse import ArgumentParser
|
|
|
7
7
|
from argparse import Namespace
|
|
8
8
|
from importlib import metadata
|
|
9
9
|
|
|
10
|
-
from devservices.commands.check_for_update import check_for_update
|
|
11
10
|
from devservices.constants import DEVSERVICES_DOWNLOAD_URL
|
|
12
11
|
from devservices.exceptions import BinaryInstallError
|
|
13
12
|
from devservices.exceptions import DevservicesUpdateError
|
|
13
|
+
from devservices.utils.check_for_update import check_for_update
|
|
14
14
|
from devservices.utils.console import Console
|
|
15
15
|
from devservices.utils.install_binary import install_binary
|
|
16
16
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from datetime import timedelta
|
|
4
5
|
|
|
5
6
|
MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
|
|
6
7
|
DEVSERVICES_DIR_NAME = "devservices"
|
|
@@ -12,6 +13,7 @@ DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
|
|
|
12
13
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "dependencies")
|
|
13
14
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
|
|
14
15
|
STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
|
|
16
|
+
DEVSERVICES_ORCHESTRATOR_LABEL = "orchestrator=devservices"
|
|
15
17
|
DOCKER_COMPOSE_COMMAND_LENGTH = 7
|
|
16
18
|
|
|
17
19
|
DEPENDENCY_CONFIG_VERSION = "v1"
|
|
@@ -21,9 +23,18 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
|
|
|
21
23
|
"core.sparseCheckout": "true",
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
DEVSERVICES_RELEASES_URL = (
|
|
27
|
+
"https://api.github.com/repos/getsentry/devservices/releases/latest"
|
|
28
|
+
)
|
|
24
29
|
DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
|
|
25
30
|
DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
|
|
26
31
|
BINARY_PERMISSIONS = 0o755
|
|
27
32
|
MAX_LOG_LINES = "100"
|
|
28
33
|
LOGGER_NAME = "devservices"
|
|
29
34
|
DOCKER_NETWORK_NAME = "devservices"
|
|
35
|
+
|
|
36
|
+
# Latest Version Cache
|
|
37
|
+
DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
|
|
38
|
+
DEVSERVICES_CACHE_DIR, "latest_version.txt"
|
|
39
|
+
)
|
|
40
|
+
DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
|
|
@@ -57,8 +57,8 @@ class DockerComposeInstallationError(BinaryInstallError):
|
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
class
|
|
61
|
-
"""Base class for Docker
|
|
60
|
+
class DockerError(Exception):
|
|
61
|
+
"""Base class for Docker related errors."""
|
|
62
62
|
|
|
63
63
|
def __init__(self, command: str, returncode: int, stdout: str, stderr: str):
|
|
64
64
|
self.command = command
|
|
@@ -67,6 +67,12 @@ class DockerComposeError(Exception):
|
|
|
67
67
|
self.stderr = stderr
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class DockerComposeError(DockerError):
|
|
71
|
+
"""Base class for Docker Compose related errors."""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
70
76
|
class ModeDoesNotExistError(Exception):
|
|
71
77
|
"""Raised when a mode does not exist."""
|
|
72
78
|
|
|
@@ -23,10 +23,10 @@ from devservices.commands import purge
|
|
|
23
23
|
from devservices.commands import status
|
|
24
24
|
from devservices.commands import up
|
|
25
25
|
from devservices.commands import update
|
|
26
|
-
from devservices.commands.check_for_update import check_for_update
|
|
27
26
|
from devservices.constants import LOGGER_NAME
|
|
28
27
|
from devservices.exceptions import DockerComposeInstallationError
|
|
29
28
|
from devservices.exceptions import DockerDaemonNotRunningError
|
|
29
|
+
from devservices.utils.check_for_update import check_for_update
|
|
30
30
|
from devservices.utils.console import Console
|
|
31
31
|
from devservices.utils.docker_compose import check_docker_compose_version
|
|
32
32
|
|
|
@@ -102,7 +102,7 @@ def main() -> None:
|
|
|
102
102
|
else:
|
|
103
103
|
parser.print_help()
|
|
104
104
|
|
|
105
|
-
if args.command != "update":
|
|
105
|
+
if args.command != "update" and os.environ.get("CI") != "true":
|
|
106
106
|
newest_version = check_for_update()
|
|
107
107
|
if newest_version != current_version:
|
|
108
108
|
console.warning(
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from urllib.request import urlopen
|
|
8
|
+
|
|
9
|
+
from devservices.constants import DEVSERVICES_CACHE_DIR
|
|
10
|
+
from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_FILE
|
|
11
|
+
from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_TTL
|
|
12
|
+
from devservices.constants import DEVSERVICES_RELEASES_URL
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _delete_cached_version() -> None:
|
|
16
|
+
if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
|
|
17
|
+
os.remove(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_cache_age() -> timedelta:
|
|
21
|
+
if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
|
|
22
|
+
file_modification_time = datetime.fromtimestamp(
|
|
23
|
+
os.path.getmtime(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
|
|
24
|
+
)
|
|
25
|
+
return datetime.now() - file_modification_time
|
|
26
|
+
return timedelta.max
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_cached_version() -> str | None:
|
|
30
|
+
cache_age = _get_cache_age()
|
|
31
|
+
if cache_age < DEVSERVICES_LATEST_VERSION_CACHE_TTL:
|
|
32
|
+
with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "r", encoding="utf-8") as f:
|
|
33
|
+
return f.read()
|
|
34
|
+
else:
|
|
35
|
+
_delete_cached_version()
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _set_cached_version(latest_version: str) -> None:
|
|
40
|
+
with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "w", encoding="utf-8") as f:
|
|
41
|
+
f.write(latest_version)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_for_update() -> str | None:
|
|
45
|
+
os.makedirs(DEVSERVICES_CACHE_DIR, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
cached_version = _get_cached_version()
|
|
48
|
+
if cached_version is not None:
|
|
49
|
+
return cached_version
|
|
50
|
+
|
|
51
|
+
with urlopen(DEVSERVICES_RELEASES_URL) as response:
|
|
52
|
+
if response.status == 200:
|
|
53
|
+
data = json.loads(response.read())
|
|
54
|
+
latest_version = str(data["tag_name"])
|
|
55
|
+
|
|
56
|
+
_set_cached_version(latest_version)
|
|
57
|
+
|
|
58
|
+
return latest_version
|
|
59
|
+
return None
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
6
|
+
from devservices.exceptions import DockerError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_docker_daemon_running() -> None:
|
|
10
|
+
"""Checks if the Docker daemon is running. Raises DockerDaemonNotRunningError if not."""
|
|
11
|
+
try:
|
|
12
|
+
subprocess.run(
|
|
13
|
+
["docker", "info"],
|
|
14
|
+
capture_output=True,
|
|
15
|
+
text=True,
|
|
16
|
+
check=True,
|
|
17
|
+
)
|
|
18
|
+
except subprocess.CalledProcessError as e:
|
|
19
|
+
raise DockerDaemonNotRunningError from e
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_matching_containers(label: str) -> list[str]:
|
|
23
|
+
"""
|
|
24
|
+
Returns a list of container IDs with the given label
|
|
25
|
+
"""
|
|
26
|
+
check_docker_daemon_running()
|
|
27
|
+
try:
|
|
28
|
+
return (
|
|
29
|
+
subprocess.check_output(
|
|
30
|
+
[
|
|
31
|
+
"docker",
|
|
32
|
+
"ps",
|
|
33
|
+
"-q",
|
|
34
|
+
"--filter",
|
|
35
|
+
f"label={label}",
|
|
36
|
+
],
|
|
37
|
+
stderr=subprocess.DEVNULL,
|
|
38
|
+
)
|
|
39
|
+
.decode()
|
|
40
|
+
.strip()
|
|
41
|
+
.splitlines()
|
|
42
|
+
)
|
|
43
|
+
except subprocess.CalledProcessError as e:
|
|
44
|
+
raise DockerError(
|
|
45
|
+
command=f"docker ps -q --filter label={label}",
|
|
46
|
+
returncode=e.returncode,
|
|
47
|
+
stdout=e.stdout,
|
|
48
|
+
stderr=e.stderr,
|
|
49
|
+
) from e
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def stop_matching_containers(label: str, should_remove: bool = False) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Stops all containers with the given label.
|
|
55
|
+
If should_remove is True, the containers will be removed.
|
|
56
|
+
"""
|
|
57
|
+
matching_containers = get_matching_containers(label)
|
|
58
|
+
if len(matching_containers) == 0:
|
|
59
|
+
return
|
|
60
|
+
try:
|
|
61
|
+
subprocess.run(
|
|
62
|
+
["docker", "stop"] + matching_containers,
|
|
63
|
+
check=True,
|
|
64
|
+
stdout=subprocess.DEVNULL,
|
|
65
|
+
stderr=subprocess.DEVNULL,
|
|
66
|
+
)
|
|
67
|
+
except subprocess.CalledProcessError as e:
|
|
68
|
+
raise DockerError(
|
|
69
|
+
command=f"docker stop {' '.join(matching_containers)}",
|
|
70
|
+
returncode=e.returncode,
|
|
71
|
+
stdout=e.stdout,
|
|
72
|
+
stderr=e.stderr,
|
|
73
|
+
) from e
|
|
74
|
+
if should_remove:
|
|
75
|
+
try:
|
|
76
|
+
subprocess.run(
|
|
77
|
+
["docker", "rm"] + matching_containers,
|
|
78
|
+
check=True,
|
|
79
|
+
stdout=subprocess.DEVNULL,
|
|
80
|
+
stderr=subprocess.DEVNULL,
|
|
81
|
+
)
|
|
82
|
+
except subprocess.CalledProcessError as e:
|
|
83
|
+
raise DockerError(
|
|
84
|
+
command=f"docker rm {' '.join(matching_containers)}",
|
|
85
|
+
returncode=e.returncode,
|
|
86
|
+
stdout=e.stdout,
|
|
87
|
+
stderr=e.stderr,
|
|
88
|
+
) from e
|
|
@@ -13,7 +13,6 @@ devservices.egg-info/entry_points.txt
|
|
|
13
13
|
devservices.egg-info/requires.txt
|
|
14
14
|
devservices.egg-info/top_level.txt
|
|
15
15
|
devservices/commands/__init__.py
|
|
16
|
-
devservices/commands/check_for_update.py
|
|
17
16
|
devservices/commands/down.py
|
|
18
17
|
devservices/commands/list_dependencies.py
|
|
19
18
|
devservices/commands/list_services.py
|
|
@@ -24,6 +23,7 @@ devservices/commands/up.py
|
|
|
24
23
|
devservices/commands/update.py
|
|
25
24
|
devservices/configs/service_config.py
|
|
26
25
|
devservices/utils/__init__.py
|
|
26
|
+
devservices/utils/check_for_update.py
|
|
27
27
|
devservices/utils/console.py
|
|
28
28
|
devservices/utils/dependencies.py
|
|
29
29
|
devservices/utils/devenv.py
|
|
@@ -46,6 +46,7 @@ tests/commands/test_status.py
|
|
|
46
46
|
tests/commands/test_up.py
|
|
47
47
|
tests/commands/test_update.py
|
|
48
48
|
tests/configs/test_service_config.py
|
|
49
|
+
tests/utils/test_check_for_update.py
|
|
49
50
|
tests/utils/test_dependencies.py
|
|
50
51
|
tests/utils/test_docker.py
|
|
51
52
|
tests/utils/test_docker_compose.py
|
|
@@ -1,47 +1,82 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import builtins
|
|
4
3
|
from argparse import Namespace
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from unittest import mock
|
|
7
6
|
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
8
9
|
from devservices.commands.purge import purge
|
|
10
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
11
|
+
from devservices.exceptions import DockerError
|
|
9
12
|
from devservices.utils.state import State
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
@mock.patch("devservices.commands.purge.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
@mock.patch("devservices.commands.purge.stop_matching_containers")
|
|
16
|
+
@mock.patch("devservices.commands.purge.subprocess.run")
|
|
17
|
+
def test_purge_docker_daemon_not_running(
|
|
18
|
+
mock_run: mock.Mock,
|
|
19
|
+
mock_stop_matching_containers: mock.Mock,
|
|
20
|
+
capsys: pytest.CaptureFixture[str],
|
|
21
|
+
tmp_path: Path,
|
|
15
22
|
) -> None:
|
|
23
|
+
mock_stop_matching_containers.side_effect = DockerDaemonNotRunningError()
|
|
16
24
|
with (
|
|
17
25
|
mock.patch(
|
|
18
26
|
"devservices.commands.purge.DEVSERVICES_CACHE_DIR",
|
|
19
27
|
str(tmp_path / ".devservices-cache"),
|
|
20
28
|
),
|
|
21
29
|
mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
|
|
22
|
-
mock.patch
|
|
30
|
+
mock.patch(
|
|
31
|
+
"devservices.commands.purge.subprocess.check_output",
|
|
32
|
+
return_value=b"",
|
|
33
|
+
),
|
|
23
34
|
):
|
|
35
|
+
# Create a cache file to test purging
|
|
36
|
+
cache_dir = tmp_path / ".devservices-cache"
|
|
37
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
cache_file = tmp_path / ".devservices-cache" / "test.txt"
|
|
39
|
+
cache_file.write_text("This is a test cache file.")
|
|
40
|
+
|
|
41
|
+
state = State()
|
|
42
|
+
state.update_started_service("test-service", "test-mode")
|
|
43
|
+
|
|
44
|
+
assert cache_file.exists()
|
|
45
|
+
assert state.get_started_services() == ["test-service"]
|
|
46
|
+
|
|
24
47
|
args = Namespace()
|
|
25
48
|
purge(args)
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
assert not cache_file.exists()
|
|
51
|
+
assert state.get_started_services() == []
|
|
28
52
|
|
|
53
|
+
mock_stop_matching_containers.assert_called_once()
|
|
54
|
+
mock_run.assert_not_called()
|
|
55
|
+
|
|
56
|
+
captured = capsys.readouterr()
|
|
57
|
+
assert (
|
|
58
|
+
"The docker daemon is not running, no containers to stop"
|
|
59
|
+
in captured.out.strip()
|
|
60
|
+
)
|
|
29
61
|
|
|
30
|
-
|
|
62
|
+
|
|
63
|
+
@mock.patch("devservices.commands.purge.stop_matching_containers")
|
|
31
64
|
@mock.patch("devservices.commands.purge.subprocess.run")
|
|
32
|
-
def
|
|
33
|
-
mock_run: mock.Mock,
|
|
65
|
+
def test_purge_docker_daemon_docker_error(
|
|
66
|
+
mock_run: mock.Mock,
|
|
67
|
+
mock_stop_matching_containers: mock.Mock,
|
|
68
|
+
capsys: pytest.CaptureFixture[str],
|
|
69
|
+
tmp_path: Path,
|
|
34
70
|
) -> None:
|
|
71
|
+
mock_stop_matching_containers.side_effect = DockerError(
|
|
72
|
+
"command", 1, "output", "stderr"
|
|
73
|
+
)
|
|
35
74
|
with (
|
|
36
75
|
mock.patch(
|
|
37
76
|
"devservices.commands.purge.DEVSERVICES_CACHE_DIR",
|
|
38
77
|
str(tmp_path / ".devservices-cache"),
|
|
39
78
|
),
|
|
40
79
|
mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
|
|
41
|
-
mock.patch.object(builtins, "input", lambda _: "yes"),
|
|
42
|
-
mock.patch(
|
|
43
|
-
"devservices.utils.docker.check_docker_daemon_running", return_value=None
|
|
44
|
-
),
|
|
45
80
|
mock.patch(
|
|
46
81
|
"devservices.commands.purge.subprocess.check_output",
|
|
47
82
|
return_value=b"",
|
|
@@ -60,19 +95,26 @@ def test_purge_with_cache_and_state_and_no_running_containers_confirmed(
|
|
|
60
95
|
assert state.get_started_services() == ["test-service"]
|
|
61
96
|
|
|
62
97
|
args = Namespace()
|
|
63
|
-
|
|
98
|
+
with pytest.raises(SystemExit):
|
|
99
|
+
purge(args)
|
|
64
100
|
|
|
65
101
|
assert not cache_file.exists()
|
|
66
102
|
assert state.get_started_services() == []
|
|
67
103
|
|
|
68
|
-
|
|
104
|
+
mock_stop_matching_containers.assert_called_once()
|
|
69
105
|
mock_run.assert_not_called()
|
|
70
106
|
|
|
107
|
+
captured = capsys.readouterr()
|
|
108
|
+
assert (
|
|
109
|
+
"Failed to stop running devservices containers stderr"
|
|
110
|
+
in captured.out.strip()
|
|
111
|
+
)
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
|
|
114
|
+
@mock.patch("devservices.commands.purge.stop_matching_containers")
|
|
73
115
|
@mock.patch("devservices.commands.purge.subprocess.run")
|
|
74
|
-
def
|
|
75
|
-
mock_run: mock.Mock,
|
|
116
|
+
def test_purge_with_cache_and_state_and_no_running_containers(
|
|
117
|
+
mock_run: mock.Mock, mock_stop_matching_containers: mock.Mock, tmp_path: Path
|
|
76
118
|
) -> None:
|
|
77
119
|
with (
|
|
78
120
|
mock.patch(
|
|
@@ -80,13 +122,12 @@ def test_purge_with_cache_and_state_and_running_containers_with_networks_confirm
|
|
|
80
122
|
str(tmp_path / ".devservices-cache"),
|
|
81
123
|
),
|
|
82
124
|
mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
|
|
83
|
-
mock.patch.object(builtins, "input", lambda _: "yes"),
|
|
84
125
|
mock.patch(
|
|
85
126
|
"devservices.utils.docker.check_docker_daemon_running", return_value=None
|
|
86
127
|
),
|
|
87
128
|
mock.patch(
|
|
88
129
|
"devservices.commands.purge.subprocess.check_output",
|
|
89
|
-
return_value=b"
|
|
130
|
+
return_value=b"",
|
|
90
131
|
),
|
|
91
132
|
):
|
|
92
133
|
# Create a cache file to test purging
|
|
@@ -107,35 +148,14 @@ def test_purge_with_cache_and_state_and_running_containers_with_networks_confirm
|
|
|
107
148
|
assert not cache_file.exists()
|
|
108
149
|
assert state.get_started_services() == []
|
|
109
150
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
mock.call(
|
|
113
|
-
["docker", "network", "rm", "abc"],
|
|
114
|
-
check=True,
|
|
115
|
-
stdout=mock.ANY,
|
|
116
|
-
stderr=mock.ANY,
|
|
117
|
-
),
|
|
118
|
-
mock.call(
|
|
119
|
-
["docker", "network", "rm", "def"],
|
|
120
|
-
check=True,
|
|
121
|
-
stdout=mock.ANY,
|
|
122
|
-
stderr=mock.ANY,
|
|
123
|
-
),
|
|
124
|
-
mock.call(
|
|
125
|
-
["docker", "network", "rm", "ghe"],
|
|
126
|
-
check=True,
|
|
127
|
-
stdout=mock.ANY,
|
|
128
|
-
stderr=mock.ANY,
|
|
129
|
-
),
|
|
130
|
-
]
|
|
131
|
-
)
|
|
132
|
-
mock_stop_all_running_containers.assert_called_once()
|
|
151
|
+
mock_stop_matching_containers.assert_called_once()
|
|
152
|
+
mock_run.assert_not_called()
|
|
133
153
|
|
|
134
154
|
|
|
135
|
-
@mock.patch("devservices.commands.purge.
|
|
155
|
+
@mock.patch("devservices.commands.purge.stop_matching_containers")
|
|
136
156
|
@mock.patch("devservices.commands.purge.subprocess.run")
|
|
137
|
-
def
|
|
138
|
-
mock_run: mock.Mock,
|
|
157
|
+
def test_purge_with_cache_and_state_and_running_containers_with_networks(
|
|
158
|
+
mock_run: mock.Mock, mock_stop_matching_containers: mock.Mock, tmp_path: Path
|
|
139
159
|
) -> None:
|
|
140
160
|
with (
|
|
141
161
|
mock.patch(
|
|
@@ -143,10 +163,13 @@ def test_purge_with_cache_and_state_and_running_containers_not_confirmed(
|
|
|
143
163
|
str(tmp_path / ".devservices-cache"),
|
|
144
164
|
),
|
|
145
165
|
mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
|
|
146
|
-
mock.patch.object(builtins, "input", lambda _: "no"),
|
|
147
166
|
mock.patch(
|
|
148
167
|
"devservices.utils.docker.check_docker_daemon_running", return_value=None
|
|
149
168
|
),
|
|
169
|
+
mock.patch(
|
|
170
|
+
"devservices.commands.purge.subprocess.check_output",
|
|
171
|
+
return_value=b"abc\ndef\nghe\n",
|
|
172
|
+
),
|
|
150
173
|
):
|
|
151
174
|
# Create a cache file to test purging
|
|
152
175
|
cache_dir = tmp_path / ".devservices-cache"
|
|
@@ -157,11 +180,35 @@ def test_purge_with_cache_and_state_and_running_containers_not_confirmed(
|
|
|
157
180
|
state = State()
|
|
158
181
|
state.update_started_service("test-service", "test-mode")
|
|
159
182
|
|
|
183
|
+
assert cache_file.exists()
|
|
184
|
+
assert state.get_started_services() == ["test-service"]
|
|
185
|
+
|
|
160
186
|
args = Namespace()
|
|
161
187
|
purge(args)
|
|
162
188
|
|
|
163
|
-
assert cache_file.exists()
|
|
164
|
-
assert state.get_started_services() == [
|
|
189
|
+
assert not cache_file.exists()
|
|
190
|
+
assert state.get_started_services() == []
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
mock_stop_matching_containers.assert_called_once()
|
|
193
|
+
mock_run.assert_has_calls(
|
|
194
|
+
[
|
|
195
|
+
mock.call(
|
|
196
|
+
["docker", "network", "rm", "abc"],
|
|
197
|
+
check=True,
|
|
198
|
+
stdout=mock.ANY,
|
|
199
|
+
stderr=mock.ANY,
|
|
200
|
+
),
|
|
201
|
+
mock.call(
|
|
202
|
+
["docker", "network", "rm", "def"],
|
|
203
|
+
check=True,
|
|
204
|
+
stdout=mock.ANY,
|
|
205
|
+
stderr=mock.ANY,
|
|
206
|
+
),
|
|
207
|
+
mock.call(
|
|
208
|
+
["docker", "network", "rm", "ghe"],
|
|
209
|
+
check=True,
|
|
210
|
+
stdout=mock.ANY,
|
|
211
|
+
stderr=mock.ANY,
|
|
212
|
+
),
|
|
213
|
+
]
|
|
214
|
+
)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest import mock
|
|
7
|
+
|
|
8
|
+
from freezegun import freeze_time
|
|
9
|
+
|
|
10
|
+
from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_TTL
|
|
11
|
+
from devservices.constants import DEVSERVICES_RELEASES_URL
|
|
12
|
+
from devservices.utils.check_for_update import check_for_update
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@mock.patch("devservices.utils.check_for_update.urlopen")
|
|
16
|
+
def test_check_for_update_not_cached(mock_urlopen: mock.Mock, tmp_path: Path) -> None:
|
|
17
|
+
mock_response = mock.mock_open(read_data=b'{"tag_name": "1.0.0"}').return_value
|
|
18
|
+
mock_response.status = 200
|
|
19
|
+
mock_urlopen.side_effect = [mock_response]
|
|
20
|
+
cache_dir = tmp_path / "cache"
|
|
21
|
+
cached_file = cache_dir / "latest_version.txt"
|
|
22
|
+
with (
|
|
23
|
+
freeze_time("2024-05-14 05:43:21"),
|
|
24
|
+
mock.patch(
|
|
25
|
+
"devservices.utils.check_for_update.os.path.getmtime",
|
|
26
|
+
return_value=datetime.now().timestamp(),
|
|
27
|
+
),
|
|
28
|
+
mock.patch(
|
|
29
|
+
"devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
|
|
30
|
+
str(cache_dir),
|
|
31
|
+
),
|
|
32
|
+
mock.patch(
|
|
33
|
+
"devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
|
|
34
|
+
str(cached_file),
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
assert check_for_update() == "1.0.0"
|
|
38
|
+
mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
|
|
39
|
+
|
|
40
|
+
with cached_file.open("r") as f:
|
|
41
|
+
cached_version = f.read()
|
|
42
|
+
assert cached_version == "1.0.0"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mock.patch("devservices.utils.check_for_update.urlopen")
|
|
46
|
+
def test_check_for_update_no_cache_not_ok(
|
|
47
|
+
mock_urlopen: mock.Mock, tmp_path: Path
|
|
48
|
+
) -> None:
|
|
49
|
+
mock_response = mock.mock_open().return_value
|
|
50
|
+
mock_response.status = 500
|
|
51
|
+
mock_urlopen.side_effect = [mock_response]
|
|
52
|
+
cache_dir = tmp_path / "cache"
|
|
53
|
+
cached_file = cache_dir / "latest_version.txt"
|
|
54
|
+
with (
|
|
55
|
+
mock.patch(
|
|
56
|
+
"devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
|
|
57
|
+
str(cache_dir),
|
|
58
|
+
),
|
|
59
|
+
mock.patch(
|
|
60
|
+
"devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
|
|
61
|
+
str(cached_file),
|
|
62
|
+
),
|
|
63
|
+
):
|
|
64
|
+
assert check_for_update() is None
|
|
65
|
+
mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
|
|
66
|
+
|
|
67
|
+
assert not cached_file.exists()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@mock.patch("devservices.utils.check_for_update.urlopen")
|
|
71
|
+
def test_check_for_update_cached_fresh(mock_urlopen: mock.Mock, tmp_path: Path) -> None:
|
|
72
|
+
cache_dir = tmp_path / "cache"
|
|
73
|
+
cached_file = cache_dir / "latest_version.txt"
|
|
74
|
+
with (
|
|
75
|
+
freeze_time("2024-05-14 05:43:21"),
|
|
76
|
+
mock.patch(
|
|
77
|
+
"devservices.utils.check_for_update.os.path.getmtime",
|
|
78
|
+
return_value=(
|
|
79
|
+
datetime.now()
|
|
80
|
+
- DEVSERVICES_LATEST_VERSION_CACHE_TTL
|
|
81
|
+
+ timedelta(minutes=1)
|
|
82
|
+
).timestamp(),
|
|
83
|
+
),
|
|
84
|
+
mock.patch(
|
|
85
|
+
"devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
|
|
86
|
+
str(cache_dir),
|
|
87
|
+
),
|
|
88
|
+
mock.patch(
|
|
89
|
+
"devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
|
|
90
|
+
str(cached_file),
|
|
91
|
+
),
|
|
92
|
+
):
|
|
93
|
+
cache_dir.mkdir()
|
|
94
|
+
cached_file.write_text("1.0.0")
|
|
95
|
+
assert check_for_update() == "1.0.0"
|
|
96
|
+
mock_urlopen.assert_not_called()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@mock.patch("devservices.utils.check_for_update.urlopen")
|
|
100
|
+
def test_check_for_update_cached_stale_without_update(
|
|
101
|
+
mock_urlopen: mock.Mock, tmp_path: Path
|
|
102
|
+
) -> None:
|
|
103
|
+
mock_response = mock.mock_open(read_data=b'{"tag_name": "1.0.0"}').return_value
|
|
104
|
+
mock_response.status = 200
|
|
105
|
+
mock_urlopen.side_effect = [mock_response]
|
|
106
|
+
cache_dir = tmp_path / "cache"
|
|
107
|
+
cached_file = cache_dir / "latest_version.txt"
|
|
108
|
+
with (
|
|
109
|
+
freeze_time("2024-05-14 05:43:21"),
|
|
110
|
+
mock.patch(
|
|
111
|
+
"devservices.utils.check_for_update.os.path.getmtime",
|
|
112
|
+
return_value=(
|
|
113
|
+
datetime.now() - DEVSERVICES_LATEST_VERSION_CACHE_TTL
|
|
114
|
+
).timestamp(),
|
|
115
|
+
),
|
|
116
|
+
mock.patch(
|
|
117
|
+
"devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
|
|
118
|
+
str(cache_dir),
|
|
119
|
+
),
|
|
120
|
+
mock.patch(
|
|
121
|
+
"devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
|
|
122
|
+
str(cached_file),
|
|
123
|
+
),
|
|
124
|
+
):
|
|
125
|
+
cache_dir.mkdir()
|
|
126
|
+
cached_file.write_text("1.0.0")
|
|
127
|
+
assert check_for_update() == "1.0.0"
|
|
128
|
+
mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
|
|
129
|
+
|
|
130
|
+
with cached_file.open("r") as f:
|
|
131
|
+
cached_version = f.read()
|
|
132
|
+
assert cached_version == "1.0.0"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mock.patch("devservices.utils.check_for_update.urlopen")
|
|
136
|
+
def test_check_for_update_cached_stale_with_update(
|
|
137
|
+
mock_urlopen: mock.Mock, tmp_path: Path
|
|
138
|
+
) -> None:
|
|
139
|
+
mock_response = mock.mock_open(read_data=b'{"tag_name": "1.0.1"}').return_value
|
|
140
|
+
mock_response.status = 200
|
|
141
|
+
mock_urlopen.side_effect = [mock_response]
|
|
142
|
+
cache_dir = tmp_path / "cache"
|
|
143
|
+
cached_file = cache_dir / "latest_version.txt"
|
|
144
|
+
with (
|
|
145
|
+
freeze_time("2024-05-14 05:43:21"),
|
|
146
|
+
mock.patch(
|
|
147
|
+
"devservices.utils.check_for_update.os.path.getmtime",
|
|
148
|
+
return_value=(
|
|
149
|
+
datetime.now()
|
|
150
|
+
- DEVSERVICES_LATEST_VERSION_CACHE_TTL
|
|
151
|
+
- timedelta(minutes=1)
|
|
152
|
+
).timestamp(),
|
|
153
|
+
),
|
|
154
|
+
mock.patch(
|
|
155
|
+
"devservices.utils.check_for_update.DEVSERVICES_CACHE_DIR",
|
|
156
|
+
str(cache_dir),
|
|
157
|
+
),
|
|
158
|
+
mock.patch(
|
|
159
|
+
"devservices.utils.check_for_update.DEVSERVICES_LATEST_VERSION_CACHE_FILE",
|
|
160
|
+
str(cached_file),
|
|
161
|
+
),
|
|
162
|
+
):
|
|
163
|
+
cache_dir.mkdir()
|
|
164
|
+
cached_file.write_text("1.0.0")
|
|
165
|
+
assert check_for_update() == "1.0.1"
|
|
166
|
+
mock_urlopen.assert_called_once_with(DEVSERVICES_RELEASES_URL)
|
|
167
|
+
|
|
168
|
+
with cached_file.open("r") as f:
|
|
169
|
+
cached_data = f.read()
|
|
170
|
+
assert cached_data == "1.0.1"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
|
|
9
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
10
|
+
from devservices.exceptions import DockerError
|
|
11
|
+
from devservices.utils.docker import check_docker_daemon_running
|
|
12
|
+
from devservices.utils.docker import get_matching_containers
|
|
13
|
+
from devservices.utils.docker import stop_matching_containers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mock.patch("subprocess.run")
|
|
17
|
+
def test_check_docker_daemon_running_error(mock_run: mock.Mock) -> None:
|
|
18
|
+
mock_run.side_effect = subprocess.CalledProcessError(1, "cmd")
|
|
19
|
+
with pytest.raises(DockerDaemonNotRunningError):
|
|
20
|
+
check_docker_daemon_running()
|
|
21
|
+
mock_run.assert_called_once_with(
|
|
22
|
+
["docker", "info"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
check=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@mock.patch("subprocess.run")
|
|
30
|
+
def test_check_docker_daemon_running(mock_run: mock.Mock) -> None:
|
|
31
|
+
check_docker_daemon_running()
|
|
32
|
+
mock_run.assert_called_once_with(
|
|
33
|
+
["docker", "info"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
check=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mock.patch("subprocess.check_output")
|
|
41
|
+
@mock.patch("devservices.utils.docker.check_docker_daemon_running")
|
|
42
|
+
def test_get_matching_containers(
|
|
43
|
+
mock_check_docker_daemon_running: mock.Mock,
|
|
44
|
+
mock_check_output: mock.Mock,
|
|
45
|
+
) -> None:
|
|
46
|
+
mock_check_docker_daemon_running.return_value = None
|
|
47
|
+
mock_check_output.return_value = b""
|
|
48
|
+
get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
|
|
49
|
+
mock_check_docker_daemon_running.assert_called_once()
|
|
50
|
+
mock_check_output.assert_called_once_with(
|
|
51
|
+
["docker", "ps", "-q", "--filter", f"label={DEVSERVICES_ORCHESTRATOR_LABEL}"],
|
|
52
|
+
stderr=subprocess.DEVNULL,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@mock.patch("subprocess.check_output")
|
|
57
|
+
@mock.patch("devservices.utils.docker.check_docker_daemon_running")
|
|
58
|
+
def test_get_matching_containers_docker_daemon_not_running(
|
|
59
|
+
mock_check_docker_daemon_running: mock.Mock,
|
|
60
|
+
mock_check_output: mock.Mock,
|
|
61
|
+
) -> None:
|
|
62
|
+
mock_check_docker_daemon_running.side_effect = DockerDaemonNotRunningError()
|
|
63
|
+
with pytest.raises(DockerDaemonNotRunningError):
|
|
64
|
+
get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
|
|
65
|
+
mock_check_docker_daemon_running.assert_called_once()
|
|
66
|
+
mock_check_output.assert_not_called()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mock.patch("subprocess.check_output")
|
|
70
|
+
@mock.patch("devservices.utils.docker.check_docker_daemon_running")
|
|
71
|
+
def test_get_matching_containers_error(
|
|
72
|
+
mock_check_docker_daemon_running: mock.Mock,
|
|
73
|
+
mock_check_output: mock.Mock,
|
|
74
|
+
) -> None:
|
|
75
|
+
mock_check_docker_daemon_running.return_value = None
|
|
76
|
+
mock_check_output.side_effect = subprocess.CalledProcessError(1, "cmd")
|
|
77
|
+
with pytest.raises(DockerError):
|
|
78
|
+
get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
|
|
79
|
+
mock_check_docker_daemon_running.assert_called_once()
|
|
80
|
+
mock_check_output.assert_called_once_with(
|
|
81
|
+
["docker", "ps", "-q", "--filter", f"label={DEVSERVICES_ORCHESTRATOR_LABEL}"],
|
|
82
|
+
stderr=subprocess.DEVNULL,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@mock.patch("subprocess.run")
|
|
87
|
+
@mock.patch("devservices.utils.docker.get_matching_containers")
|
|
88
|
+
def test_stop_matching_containers_should_not_remove(
|
|
89
|
+
mock_get_matching_containers: mock.Mock,
|
|
90
|
+
mock_run: mock.Mock,
|
|
91
|
+
) -> None:
|
|
92
|
+
mock_get_matching_containers.return_value = ["container1", "container2"]
|
|
93
|
+
stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=False)
|
|
94
|
+
mock_run.assert_called_once_with(
|
|
95
|
+
["docker", "stop", "container1", "container2"],
|
|
96
|
+
check=True,
|
|
97
|
+
stdout=subprocess.DEVNULL,
|
|
98
|
+
stderr=subprocess.DEVNULL,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@mock.patch("subprocess.run")
|
|
103
|
+
@mock.patch("devservices.utils.docker.get_matching_containers")
|
|
104
|
+
def test_stop_matching_containers_none(
|
|
105
|
+
mock_get_matching_containers: mock.Mock,
|
|
106
|
+
mock_run: mock.Mock,
|
|
107
|
+
) -> None:
|
|
108
|
+
mock_get_matching_containers.return_value = []
|
|
109
|
+
stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
|
|
110
|
+
mock_run.assert_not_called()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@mock.patch("subprocess.run")
|
|
114
|
+
@mock.patch("devservices.utils.docker.get_matching_containers")
|
|
115
|
+
def test_stop_matching_containers_should_remove(
|
|
116
|
+
mock_get_matching_containers: mock.Mock,
|
|
117
|
+
mock_run: mock.Mock,
|
|
118
|
+
) -> None:
|
|
119
|
+
mock_get_matching_containers.return_value = ["container1", "container2"]
|
|
120
|
+
stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
|
|
121
|
+
mock_run.assert_has_calls(
|
|
122
|
+
[
|
|
123
|
+
mock.call(
|
|
124
|
+
["docker", "stop", "container1", "container2"],
|
|
125
|
+
check=True,
|
|
126
|
+
stdout=subprocess.DEVNULL,
|
|
127
|
+
stderr=subprocess.DEVNULL,
|
|
128
|
+
),
|
|
129
|
+
mock.call(
|
|
130
|
+
["docker", "rm", "container1", "container2"],
|
|
131
|
+
check=True,
|
|
132
|
+
stdout=subprocess.DEVNULL,
|
|
133
|
+
stderr=subprocess.DEVNULL,
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@mock.patch("subprocess.run")
|
|
140
|
+
@mock.patch("devservices.utils.docker.get_matching_containers")
|
|
141
|
+
def test_stop_matching_containers_stop_error(
|
|
142
|
+
mock_get_matching_containers: mock.Mock,
|
|
143
|
+
mock_run: mock.Mock,
|
|
144
|
+
) -> None:
|
|
145
|
+
mock_run.side_effect = subprocess.CalledProcessError(1, "cmd")
|
|
146
|
+
mock_get_matching_containers.return_value = ["container1", "container2"]
|
|
147
|
+
with pytest.raises(DockerError):
|
|
148
|
+
stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
|
|
149
|
+
mock_run.assert_called_once_with(
|
|
150
|
+
["docker", "stop", "container1", "container2"],
|
|
151
|
+
check=True,
|
|
152
|
+
stdout=subprocess.DEVNULL,
|
|
153
|
+
stderr=subprocess.DEVNULL,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mock.patch("subprocess.run")
|
|
158
|
+
@mock.patch("devservices.utils.docker.get_matching_containers")
|
|
159
|
+
def test_stop_matching_containers_remove_error(
|
|
160
|
+
mock_get_matching_containers: mock.Mock,
|
|
161
|
+
mock_run: mock.Mock,
|
|
162
|
+
) -> None:
|
|
163
|
+
mock_run.side_effect = [None, subprocess.CalledProcessError(1, "cmd")]
|
|
164
|
+
mock_get_matching_containers.return_value = ["container1", "container2"]
|
|
165
|
+
with pytest.raises(DockerError):
|
|
166
|
+
stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
|
|
167
|
+
mock_run.assert_has_calls(
|
|
168
|
+
[
|
|
169
|
+
mock.call(
|
|
170
|
+
["docker", "stop", "container1", "container2"],
|
|
171
|
+
check=True,
|
|
172
|
+
stdout=subprocess.DEVNULL,
|
|
173
|
+
stderr=subprocess.DEVNULL,
|
|
174
|
+
),
|
|
175
|
+
mock.call(
|
|
176
|
+
["docker", "rm", "container1", "container2"],
|
|
177
|
+
check=True,
|
|
178
|
+
stdout=subprocess.DEVNULL,
|
|
179
|
+
stderr=subprocess.DEVNULL,
|
|
180
|
+
),
|
|
181
|
+
]
|
|
182
|
+
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from urllib.request import urlopen
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def check_for_update() -> str | None:
|
|
8
|
-
url = "https://api.github.com/repos/getsentry/devservices/releases/latest"
|
|
9
|
-
with urlopen(url) as response:
|
|
10
|
-
if response.status == 200:
|
|
11
|
-
data = json.loads(response.read().decode("utf-8"))
|
|
12
|
-
latest_version = str(data["tag_name"])
|
|
13
|
-
return latest_version
|
|
14
|
-
return None
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
|
|
5
|
-
from devservices.exceptions import DockerDaemonNotRunningError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def check_docker_daemon_running() -> None:
|
|
9
|
-
"""Checks if the Docker daemon is running. Raises DockerDaemonNotRunningError if not."""
|
|
10
|
-
try:
|
|
11
|
-
subprocess.run(
|
|
12
|
-
["docker", "info"],
|
|
13
|
-
capture_output=True,
|
|
14
|
-
text=True,
|
|
15
|
-
check=True,
|
|
16
|
-
)
|
|
17
|
-
except subprocess.CalledProcessError as e:
|
|
18
|
-
raise DockerDaemonNotRunningError from e
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def stop_all_running_containers() -> None:
|
|
22
|
-
check_docker_daemon_running()
|
|
23
|
-
running_containers = (
|
|
24
|
-
subprocess.check_output(["docker", "ps", "-q"], stderr=subprocess.DEVNULL)
|
|
25
|
-
.decode()
|
|
26
|
-
.strip()
|
|
27
|
-
.splitlines()
|
|
28
|
-
)
|
|
29
|
-
if len(running_containers) == 0:
|
|
30
|
-
return
|
|
31
|
-
subprocess.run(
|
|
32
|
-
["docker", "stop"] + running_containers,
|
|
33
|
-
check=True,
|
|
34
|
-
stdout=subprocess.DEVNULL,
|
|
35
|
-
stderr=subprocess.DEVNULL,
|
|
36
|
-
)
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
from unittest import mock
|
|
5
|
-
|
|
6
|
-
from devservices.utils.docker import stop_all_running_containers
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@mock.patch("subprocess.check_output")
|
|
10
|
-
@mock.patch("subprocess.run")
|
|
11
|
-
@mock.patch("devservices.utils.docker.check_docker_daemon_running")
|
|
12
|
-
def test_stop_all_running_containers_none_running(
|
|
13
|
-
mock_check_docker_daemon_running: mock.Mock,
|
|
14
|
-
mock_run: mock.Mock,
|
|
15
|
-
mock_check_output: mock.Mock,
|
|
16
|
-
) -> None:
|
|
17
|
-
mock_check_docker_daemon_running.return_value = None
|
|
18
|
-
mock_check_output.return_value = b""
|
|
19
|
-
stop_all_running_containers()
|
|
20
|
-
mock_check_docker_daemon_running.assert_called_once()
|
|
21
|
-
mock_check_output.assert_called_once_with(
|
|
22
|
-
["docker", "ps", "-q"], stderr=subprocess.DEVNULL
|
|
23
|
-
)
|
|
24
|
-
mock_run.assert_not_called()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@mock.patch("subprocess.check_output")
|
|
28
|
-
@mock.patch("subprocess.run")
|
|
29
|
-
@mock.patch("devservices.utils.docker.check_docker_daemon_running")
|
|
30
|
-
def test_stop_all_running_containers(
|
|
31
|
-
mock_check_docker_daemon_running: mock.Mock,
|
|
32
|
-
mock_run: mock.Mock,
|
|
33
|
-
mock_check_output: mock.Mock,
|
|
34
|
-
) -> None:
|
|
35
|
-
mock_check_docker_daemon_running.return_value = None
|
|
36
|
-
mock_check_output.return_value = b"container1\ncontainer2\n"
|
|
37
|
-
stop_all_running_containers()
|
|
38
|
-
mock_check_docker_daemon_running.assert_called_once()
|
|
39
|
-
mock_check_output.assert_called_once_with(
|
|
40
|
-
["docker", "ps", "-q"], stderr=subprocess.DEVNULL
|
|
41
|
-
)
|
|
42
|
-
mock_run.assert_called_once_with(
|
|
43
|
-
["docker", "stop", "container1", "container2"],
|
|
44
|
-
check=True,
|
|
45
|
-
stdout=subprocess.DEVNULL,
|
|
46
|
-
stderr=subprocess.DEVNULL,
|
|
47
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|