devservices 1.0.4__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.4 → devservices-1.0.6}/PKG-INFO +1 -1
- {devservices-1.0.4 → devservices-1.0.6}/README.md +1 -1
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/down.py +21 -8
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/list_dependencies.py +5 -2
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/list_services.py +2 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/logs.py +4 -1
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/purge.py +10 -13
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/status.py +4 -1
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/up.py +9 -57
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/update.py +1 -1
- {devservices-1.0.4 → devservices-1.0.6}/devservices/constants.py +11 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/exceptions.py +8 -2
- {devservices-1.0.4 → devservices-1.0.6}/devservices/main.py +2 -2
- devservices-1.0.6/devservices/utils/check_for_update.py +59 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/dependencies.py +37 -10
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/devenv.py +1 -1
- devservices-1.0.6/devservices/utils/docker.py +88 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/state.py +20 -11
- {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/SOURCES.txt +5 -1
- {devservices-1.0.4 → devservices-1.0.6}/pyproject.toml +1 -1
- {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_down.py +35 -3
- devservices-1.0.6/tests/commands/test_list_dependencies.py +111 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_list_services.py +4 -4
- {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_logs.py +33 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_purge.py +100 -53
- devservices-1.0.6/tests/commands/test_status.py +171 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_up.py +53 -58
- devservices-1.0.6/tests/utils/test_check_for_update.py +170 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_dependencies.py +162 -15
- devservices-1.0.6/tests/utils/test_docker.py +182 -0
- devservices-1.0.6/tests/utils/test_services.py +102 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_state.py +10 -10
- devservices-1.0.4/devservices/commands/check_for_update.py +0 -14
- devservices-1.0.4/devservices/utils/docker.py +0 -36
- devservices-1.0.4/tests/utils/test_docker.py +0 -47
- {devservices-1.0.4 → devservices-1.0.6}/LICENSE.md +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/__init__.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/console.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/docker_compose.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/services.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/setup.cfg +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/testing/__init__.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/testing/utils.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/__init__.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_update.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/conftest.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_install_binary.py +0 -0
|
@@ -56,10 +56,13 @@ def down(args: Namespace) -> None:
|
|
|
56
56
|
service_name = args.service_name
|
|
57
57
|
try:
|
|
58
58
|
service = find_matching_service(service_name)
|
|
59
|
-
except
|
|
59
|
+
except ConfigError as e:
|
|
60
60
|
capture_exception(e)
|
|
61
61
|
console.failure(str(e))
|
|
62
62
|
exit(1)
|
|
63
|
+
except ServiceNotFoundError as e:
|
|
64
|
+
console.failure(str(e))
|
|
65
|
+
exit(1)
|
|
63
66
|
|
|
64
67
|
modes = service.config.modes
|
|
65
68
|
|
|
@@ -69,24 +72,34 @@ def down(args: Namespace) -> None:
|
|
|
69
72
|
console.warning(f"{service.name} is not running")
|
|
70
73
|
exit(0)
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
mode_dependencies =
|
|
75
|
+
active_modes = state.get_active_modes_for_service(service.name)
|
|
76
|
+
mode_dependencies = set()
|
|
77
|
+
for active_mode in active_modes:
|
|
78
|
+
active_mode_dependencies = modes.get(active_mode, [])
|
|
79
|
+
mode_dependencies.update(active_mode_dependencies)
|
|
74
80
|
|
|
75
81
|
with Status(
|
|
76
82
|
lambda: console.warning(f"Stopping {service.name}"),
|
|
77
83
|
lambda: console.success(f"{service.name} stopped"),
|
|
78
84
|
) as status:
|
|
79
85
|
try:
|
|
80
|
-
remote_dependencies = install_and_verify_dependencies(
|
|
86
|
+
remote_dependencies = install_and_verify_dependencies(
|
|
87
|
+
service, modes=active_modes
|
|
88
|
+
)
|
|
89
|
+
except DependencyError as de:
|
|
90
|
+
capture_exception(de)
|
|
91
|
+
status.failure(str(de))
|
|
92
|
+
exit(1)
|
|
93
|
+
try:
|
|
94
|
+
remote_dependencies = get_non_shared_remote_dependencies(
|
|
95
|
+
service, remote_dependencies
|
|
96
|
+
)
|
|
81
97
|
except DependencyError as de:
|
|
82
98
|
capture_exception(de)
|
|
83
99
|
status.failure(str(de))
|
|
84
100
|
exit(1)
|
|
85
|
-
remote_dependencies = get_non_shared_remote_dependencies(
|
|
86
|
-
service, remote_dependencies
|
|
87
|
-
)
|
|
88
101
|
try:
|
|
89
|
-
_down(service, remote_dependencies, mode_dependencies, status)
|
|
102
|
+
_down(service, remote_dependencies, list(mode_dependencies), status)
|
|
90
103
|
except DockerComposeError as dce:
|
|
91
104
|
capture_exception(dce)
|
|
92
105
|
status.failure(f"Failed to stop {service.name}: {dce.stderr}")
|
|
@@ -32,10 +32,13 @@ def list_dependencies(args: Namespace) -> None:
|
|
|
32
32
|
|
|
33
33
|
try:
|
|
34
34
|
service = find_matching_service(service_name)
|
|
35
|
-
except
|
|
35
|
+
except ConfigError as e:
|
|
36
36
|
capture_exception(e)
|
|
37
37
|
console.failure(str(e))
|
|
38
38
|
exit(1)
|
|
39
|
+
except ServiceNotFoundError as e:
|
|
40
|
+
console.failure(str(e))
|
|
41
|
+
exit(1)
|
|
39
42
|
|
|
40
43
|
dependencies = service.config.dependencies
|
|
41
44
|
|
|
@@ -45,4 +48,4 @@ def list_dependencies(args: Namespace) -> None:
|
|
|
45
48
|
|
|
46
49
|
console.info(f"Dependencies of {service.name}:")
|
|
47
50
|
for dependency_key, dependency_info in dependencies.items():
|
|
48
|
-
console.info("-" + dependency_key + ":" + dependency_info.description)
|
|
51
|
+
console.info("- " + dependency_key + ": " + dependency_info.description)
|
|
@@ -47,7 +47,9 @@ def list_services(args: Namespace) -> None:
|
|
|
47
47
|
|
|
48
48
|
for service in services_to_show:
|
|
49
49
|
status = "running" if service.name in running_services else "stopped"
|
|
50
|
+
active_modes = state.get_active_modes_for_service(service.name)
|
|
50
51
|
console.info(f"- {service.name}")
|
|
52
|
+
console.info(f" modes: {active_modes}")
|
|
51
53
|
console.info(f" status: {status}")
|
|
52
54
|
console.info(f" location: {service.repo_path}")
|
|
53
55
|
|
|
@@ -46,10 +46,13 @@ def logs(args: Namespace) -> None:
|
|
|
46
46
|
service_name = args.service_name
|
|
47
47
|
try:
|
|
48
48
|
service = find_matching_service(service_name)
|
|
49
|
-
except
|
|
49
|
+
except ConfigError as e:
|
|
50
50
|
capture_exception(e)
|
|
51
51
|
console.failure(str(e))
|
|
52
52
|
exit(1)
|
|
53
|
+
except ServiceNotFoundError as e:
|
|
54
|
+
console.failure(str(e))
|
|
55
|
+
exit(1)
|
|
53
56
|
|
|
54
57
|
modes = service.config.modes
|
|
55
58
|
# TODO: allow custom modes to be used
|
|
@@ -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 = (
|
|
@@ -81,10 +81,13 @@ def status(args: Namespace) -> None:
|
|
|
81
81
|
service_name = args.service_name
|
|
82
82
|
try:
|
|
83
83
|
service = find_matching_service(service_name)
|
|
84
|
-
except
|
|
84
|
+
except ConfigError as e:
|
|
85
85
|
capture_exception(e)
|
|
86
86
|
console.failure(str(e))
|
|
87
87
|
exit(1)
|
|
88
|
+
except ServiceNotFoundError as e:
|
|
89
|
+
console.failure(str(e))
|
|
90
|
+
exit(1)
|
|
88
91
|
|
|
89
92
|
modes = service.config.modes
|
|
90
93
|
# TODO: allow custom modes to be used
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import concurrent.futures
|
|
4
3
|
import os
|
|
5
4
|
import subprocess
|
|
6
5
|
from argparse import _SubParsersAction
|
|
@@ -23,7 +22,6 @@ from devservices.exceptions import ServiceNotFoundError
|
|
|
23
22
|
from devservices.utils.console import Console
|
|
24
23
|
from devservices.utils.console import Status
|
|
25
24
|
from devservices.utils.dependencies import construct_dependency_graph
|
|
26
|
-
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
27
25
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
28
26
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
29
27
|
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
@@ -58,64 +56,17 @@ def up(args: Namespace) -> None:
|
|
|
58
56
|
service_name = args.service_name
|
|
59
57
|
try:
|
|
60
58
|
service = find_matching_service(service_name)
|
|
61
|
-
except
|
|
59
|
+
except ConfigError as e:
|
|
62
60
|
capture_exception(e)
|
|
63
61
|
console.failure(str(e))
|
|
64
62
|
exit(1)
|
|
63
|
+
except ServiceNotFoundError as e:
|
|
64
|
+
console.failure(str(e))
|
|
65
|
+
exit(1)
|
|
65
66
|
|
|
66
67
|
modes = service.config.modes
|
|
67
68
|
mode = args.mode
|
|
68
69
|
|
|
69
|
-
state = State()
|
|
70
|
-
started_services = state.get_started_services()
|
|
71
|
-
running_mode = state.get_mode_for_service(service.name) or "default"
|
|
72
|
-
|
|
73
|
-
# TODO: Remove this once we properly handle mode switching
|
|
74
|
-
if service.name in started_services and running_mode != mode:
|
|
75
|
-
console.warning(
|
|
76
|
-
f"Service '{service.name}' is already running in mode: '{running_mode}', restarting in mode: '{mode}'"
|
|
77
|
-
)
|
|
78
|
-
with Status() as status:
|
|
79
|
-
try:
|
|
80
|
-
remote_dependencies = install_and_verify_dependencies(
|
|
81
|
-
service, mode=running_mode
|
|
82
|
-
)
|
|
83
|
-
except DependencyError as de:
|
|
84
|
-
capture_exception(de)
|
|
85
|
-
status.failure(str(de))
|
|
86
|
-
exit(1)
|
|
87
|
-
except ModeDoesNotExistError as mde:
|
|
88
|
-
capture_exception(mde)
|
|
89
|
-
status.failure(str(mde))
|
|
90
|
-
exit(1)
|
|
91
|
-
service_config_file_path = os.path.join(
|
|
92
|
-
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
93
|
-
)
|
|
94
|
-
current_env = os.environ.copy()
|
|
95
|
-
running_mode_dependencies = modes[running_mode]
|
|
96
|
-
remote_dependencies_to_bring_down = get_non_shared_remote_dependencies(
|
|
97
|
-
service, remote_dependencies
|
|
98
|
-
)
|
|
99
|
-
down_docker_compose_commands = get_docker_compose_commands_to_run(
|
|
100
|
-
service=service,
|
|
101
|
-
remote_dependencies=list(remote_dependencies_to_bring_down),
|
|
102
|
-
current_env=current_env,
|
|
103
|
-
command="down",
|
|
104
|
-
options=[],
|
|
105
|
-
service_config_file_path=service_config_file_path,
|
|
106
|
-
mode_dependencies=running_mode_dependencies,
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
110
|
-
futures = [
|
|
111
|
-
executor.submit(run_cmd, cmd, current_env)
|
|
112
|
-
for cmd in down_docker_compose_commands
|
|
113
|
-
]
|
|
114
|
-
for future in concurrent.futures.as_completed(futures):
|
|
115
|
-
future.result()
|
|
116
|
-
|
|
117
|
-
state.remove_started_service(service.name)
|
|
118
|
-
|
|
119
70
|
with Status(
|
|
120
71
|
lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
|
|
121
72
|
lambda: console.success(f"{service.name} started"),
|
|
@@ -123,7 +74,7 @@ def up(args: Namespace) -> None:
|
|
|
123
74
|
try:
|
|
124
75
|
status.info("Retrieving dependencies")
|
|
125
76
|
remote_dependencies = install_and_verify_dependencies(
|
|
126
|
-
service, force_update_dependencies=True,
|
|
77
|
+
service, force_update_dependencies=True, modes=[mode]
|
|
127
78
|
)
|
|
128
79
|
except DependencyError as de:
|
|
129
80
|
capture_exception(de)
|
|
@@ -139,14 +90,14 @@ def up(args: Namespace) -> None:
|
|
|
139
90
|
pass
|
|
140
91
|
try:
|
|
141
92
|
mode_dependencies = modes[mode]
|
|
142
|
-
_up(service, remote_dependencies, mode_dependencies, status)
|
|
93
|
+
_up(service, [mode], remote_dependencies, mode_dependencies, status)
|
|
143
94
|
except DockerComposeError as dce:
|
|
144
95
|
capture_exception(dce)
|
|
145
96
|
status.failure(f"Failed to start {service.name}: {dce.stderr}")
|
|
146
97
|
exit(1)
|
|
147
98
|
# TODO: We should factor in healthchecks here before marking service as running
|
|
148
99
|
state = State()
|
|
149
|
-
state.
|
|
100
|
+
state.update_started_service(service.name, mode)
|
|
150
101
|
|
|
151
102
|
|
|
152
103
|
def _bring_up_dependency(
|
|
@@ -160,6 +111,7 @@ def _bring_up_dependency(
|
|
|
160
111
|
|
|
161
112
|
def _up(
|
|
162
113
|
service: Service,
|
|
114
|
+
modes: list[str],
|
|
163
115
|
remote_dependencies: set[InstalledRemoteDependency],
|
|
164
116
|
mode_dependencies: list[str],
|
|
165
117
|
status: Status,
|
|
@@ -177,7 +129,7 @@ def _up(
|
|
|
177
129
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
178
130
|
] = relative_local_dependency_directory
|
|
179
131
|
options = ["-d"]
|
|
180
|
-
dependency_graph = construct_dependency_graph(service)
|
|
132
|
+
dependency_graph = construct_dependency_graph(service, modes=modes)
|
|
181
133
|
starting_order = dependency_graph.get_starting_order()
|
|
182
134
|
sorted_remote_dependencies = sorted(
|
|
183
135
|
remote_dependencies, key=lambda dep: starting_order.index(dep.service_name)
|
|
@@ -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
|
|
@@ -160,11 +160,20 @@ class GitConfigManager:
|
|
|
160
160
|
|
|
161
161
|
|
|
162
162
|
def install_and_verify_dependencies(
|
|
163
|
-
service: Service,
|
|
163
|
+
service: Service,
|
|
164
|
+
force_update_dependencies: bool = False,
|
|
165
|
+
modes: list[str] | None = None,
|
|
164
166
|
) -> set[InstalledRemoteDependency]:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
"""
|
|
168
|
+
Install and verify dependencies for a service
|
|
169
|
+
"""
|
|
170
|
+
if modes is None:
|
|
171
|
+
modes = ["default"]
|
|
172
|
+
mode_dependencies = set()
|
|
173
|
+
for mode in modes:
|
|
174
|
+
if mode not in service.config.modes:
|
|
175
|
+
raise ModeDoesNotExistError(service_name=service.name, mode=mode)
|
|
176
|
+
mode_dependencies.update(service.config.modes[mode])
|
|
168
177
|
matching_dependencies = [
|
|
169
178
|
dependency
|
|
170
179
|
for dependency_key, dependency in list(service.config.dependencies.items())
|
|
@@ -346,8 +355,18 @@ def install_dependency(dependency: RemoteConfig) -> set[InstalledRemoteDependenc
|
|
|
346
355
|
branch=dependency.branch,
|
|
347
356
|
) from e
|
|
348
357
|
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
if dependency.mode not in installed_config.modes:
|
|
359
|
+
raise ModeDoesNotExistError(
|
|
360
|
+
service_name=installed_config.service_name,
|
|
361
|
+
mode=dependency.mode,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
active_nested_dependencies = [
|
|
365
|
+
nested_dependency
|
|
366
|
+
for nested_dependency_name, nested_dependency in installed_config.dependencies.items()
|
|
367
|
+
if nested_dependency_name in installed_config.modes[dependency.mode]
|
|
368
|
+
]
|
|
369
|
+
nested_remote_configs = _get_remote_configs(active_nested_dependencies)
|
|
351
370
|
|
|
352
371
|
installed_dependencies: set[InstalledRemoteDependency] = set(
|
|
353
372
|
[
|
|
@@ -526,15 +545,23 @@ def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
|
|
|
526
545
|
return load_service_config_from_file(dependency_repo_dir)
|
|
527
546
|
|
|
528
547
|
|
|
529
|
-
def construct_dependency_graph(service: Service) -> DependencyGraph:
|
|
548
|
+
def construct_dependency_graph(service: Service, modes: list[str]) -> DependencyGraph:
|
|
530
549
|
dependency_graph = DependencyGraph()
|
|
531
550
|
|
|
532
|
-
def _construct_dependency_graph(
|
|
551
|
+
def _construct_dependency_graph(
|
|
552
|
+
service_config: ServiceConfig, modes: list[str]
|
|
553
|
+
) -> None:
|
|
554
|
+
service_mode_dependencies = set()
|
|
555
|
+
for mode in modes:
|
|
556
|
+
service_mode_dependencies.update(service_config.modes.get(mode, []))
|
|
533
557
|
for dependency_name, dependency in service_config.dependencies.items():
|
|
558
|
+
# Skip the dependency if it's not in the modes (since it may not be installed and we don't care about it)
|
|
559
|
+
if dependency_name not in service_mode_dependencies:
|
|
560
|
+
continue
|
|
534
561
|
dependency_graph.add_edge(service_config.service_name, dependency_name)
|
|
535
562
|
if _has_remote_config(dependency.remote):
|
|
536
563
|
dependency_config = get_remote_dependency_config(dependency.remote)
|
|
537
|
-
_construct_dependency_graph(dependency_config)
|
|
564
|
+
_construct_dependency_graph(dependency_config, [dependency.remote.mode])
|
|
538
565
|
|
|
539
|
-
_construct_dependency_graph(service.config)
|
|
566
|
+
_construct_dependency_graph(service.config, modes)
|
|
540
567
|
return dependency_graph
|
|
@@ -13,7 +13,7 @@ def get_coderoot() -> str:
|
|
|
13
13
|
config_path = os.path.join(home, ".config", "sentry-devenv", "config.ini")
|
|
14
14
|
try:
|
|
15
15
|
devenv_config: ConfigParser = read_config(config_path)
|
|
16
|
-
return devenv_config.get("devenv", "coderoot", fallback="")
|
|
16
|
+
return os.path.expanduser(devenv_config.get("devenv", "coderoot", fallback=""))
|
|
17
17
|
except (FileNotFoundError, NoSectionError, NoOptionError):
|
|
18
18
|
# TODO: Handle the case where there is no config file or the coderoot is not set
|
|
19
19
|
raise Exception("Failed to read code root from config")
|
|
@@ -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
|