devservices 1.0.3__tar.gz → 1.0.5__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.3 → devservices-1.0.5}/PKG-INFO +1 -1
- {devservices-1.0.3 → devservices-1.0.5}/README.md +1 -1
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/down.py +14 -6
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/list_dependencies.py +5 -2
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/list_services.py +2 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/logs.py +5 -2
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/status.py +5 -2
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/up.py +29 -6
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/dependencies.py +114 -6
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/devenv.py +1 -1
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/docker_compose.py +2 -3
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/state.py +20 -11
- {devservices-1.0.3 → devservices-1.0.5}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.3 → devservices-1.0.5}/devservices.egg-info/SOURCES.txt +3 -0
- {devservices-1.0.3 → devservices-1.0.5}/pyproject.toml +1 -1
- {devservices-1.0.3 → devservices-1.0.5}/tests/commands/test_down.py +35 -3
- devservices-1.0.5/tests/commands/test_list_dependencies.py +111 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/commands/test_list_services.py +4 -4
- {devservices-1.0.3 → devservices-1.0.5}/tests/commands/test_logs.py +33 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/commands/test_purge.py +3 -3
- devservices-1.0.5/tests/commands/test_status.py +171 -0
- devservices-1.0.5/tests/commands/test_up.py +596 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/utils/test_dependencies.py +577 -8
- {devservices-1.0.3 → devservices-1.0.5}/tests/utils/test_docker_compose.py +127 -22
- devservices-1.0.5/tests/utils/test_services.py +102 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/utils/test_state.py +10 -10
- devservices-1.0.3/tests/commands/test_up.py +0 -338
- {devservices-1.0.3 → devservices-1.0.5}/LICENSE.md +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/check_for_update.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/purge.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/commands/update.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/constants.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/exceptions.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/main.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/console.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/docker.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices/utils/services.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/setup.cfg +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/testing/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/testing/utils.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/commands/test_update.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/conftest.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/tests/utils/test_docker.py +0 -0
- {devservices-1.0.3 → devservices-1.0.5}/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,15 +72,20 @@ 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
|
+
)
|
|
81
89
|
except DependencyError as de:
|
|
82
90
|
capture_exception(de)
|
|
83
91
|
status.failure(str(de))
|
|
@@ -86,7 +94,7 @@ def down(args: Namespace) -> None:
|
|
|
86
94
|
service, remote_dependencies
|
|
87
95
|
)
|
|
88
96
|
try:
|
|
89
|
-
_down(service, remote_dependencies, mode_dependencies, status)
|
|
97
|
+
_down(service, remote_dependencies, list(mode_dependencies), status)
|
|
90
98
|
except DockerComposeError as dce:
|
|
91
99
|
capture_exception(dce)
|
|
92
100
|
status.failure(f"Failed to stop {service.name}: {dce.stderr}")
|
|
@@ -126,7 +134,7 @@ def _down(
|
|
|
126
134
|
] = relative_local_dependency_directory
|
|
127
135
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
128
136
|
service=service,
|
|
129
|
-
remote_dependencies=remote_dependencies,
|
|
137
|
+
remote_dependencies=list(remote_dependencies),
|
|
130
138
|
current_env=current_env,
|
|
131
139
|
command="down",
|
|
132
140
|
options=[],
|
|
@@ -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
|
|
@@ -99,7 +102,7 @@ def _logs(
|
|
|
99
102
|
] = relative_local_dependency_directory
|
|
100
103
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
101
104
|
service=service,
|
|
102
|
-
remote_dependencies=remote_dependencies,
|
|
105
|
+
remote_dependencies=list(remote_dependencies),
|
|
103
106
|
current_env=current_env,
|
|
104
107
|
command="logs",
|
|
105
108
|
options=["-n", MAX_LOG_LINES],
|
|
@@ -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
|
|
@@ -137,7 +140,7 @@ def _status(
|
|
|
137
140
|
] = relative_local_dependency_directory
|
|
138
141
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
139
142
|
service=service,
|
|
140
|
-
remote_dependencies=remote_dependencies,
|
|
143
|
+
remote_dependencies=list(remote_dependencies),
|
|
141
144
|
current_env=current_env,
|
|
142
145
|
command="ps",
|
|
143
146
|
options=["--format", "json"],
|
|
@@ -21,6 +21,7 @@ from devservices.exceptions import ModeDoesNotExistError
|
|
|
21
21
|
from devservices.exceptions import ServiceNotFoundError
|
|
22
22
|
from devservices.utils.console import Console
|
|
23
23
|
from devservices.utils.console import Status
|
|
24
|
+
from devservices.utils.dependencies import construct_dependency_graph
|
|
24
25
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
25
26
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
26
27
|
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
@@ -55,22 +56,25 @@ def up(args: Namespace) -> None:
|
|
|
55
56
|
service_name = args.service_name
|
|
56
57
|
try:
|
|
57
58
|
service = find_matching_service(service_name)
|
|
58
|
-
except
|
|
59
|
+
except ConfigError as e:
|
|
59
60
|
capture_exception(e)
|
|
60
61
|
console.failure(str(e))
|
|
61
62
|
exit(1)
|
|
63
|
+
except ServiceNotFoundError as e:
|
|
64
|
+
console.failure(str(e))
|
|
65
|
+
exit(1)
|
|
62
66
|
|
|
63
67
|
modes = service.config.modes
|
|
64
68
|
mode = args.mode
|
|
65
69
|
|
|
66
70
|
with Status(
|
|
67
|
-
lambda: console.warning(f"Starting {service.name}"),
|
|
71
|
+
lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
|
|
68
72
|
lambda: console.success(f"{service.name} started"),
|
|
69
73
|
) as status:
|
|
70
74
|
try:
|
|
71
75
|
status.info("Retrieving dependencies")
|
|
72
76
|
remote_dependencies = install_and_verify_dependencies(
|
|
73
|
-
service, force_update_dependencies=True,
|
|
77
|
+
service, force_update_dependencies=True, modes=[mode]
|
|
74
78
|
)
|
|
75
79
|
except DependencyError as de:
|
|
76
80
|
capture_exception(de)
|
|
@@ -79,16 +83,21 @@ def up(args: Namespace) -> None:
|
|
|
79
83
|
except ModeDoesNotExistError as mde:
|
|
80
84
|
status.failure(str(mde))
|
|
81
85
|
exit(1)
|
|
86
|
+
try:
|
|
87
|
+
_create_devservices_network()
|
|
88
|
+
except subprocess.CalledProcessError:
|
|
89
|
+
# Network already exists, ignore the error
|
|
90
|
+
pass
|
|
82
91
|
try:
|
|
83
92
|
mode_dependencies = modes[mode]
|
|
84
|
-
_up(service, remote_dependencies, mode_dependencies, status)
|
|
93
|
+
_up(service, [mode], remote_dependencies, mode_dependencies, status)
|
|
85
94
|
except DockerComposeError as dce:
|
|
86
95
|
capture_exception(dce)
|
|
87
96
|
status.failure(f"Failed to start {service.name}: {dce.stderr}")
|
|
88
97
|
exit(1)
|
|
89
98
|
# TODO: We should factor in healthchecks here before marking service as running
|
|
90
99
|
state = State()
|
|
91
|
-
state.
|
|
100
|
+
state.update_started_service(service.name, mode)
|
|
92
101
|
|
|
93
102
|
|
|
94
103
|
def _bring_up_dependency(
|
|
@@ -102,6 +111,7 @@ def _bring_up_dependency(
|
|
|
102
111
|
|
|
103
112
|
def _up(
|
|
104
113
|
service: Service,
|
|
114
|
+
modes: list[str],
|
|
105
115
|
remote_dependencies: set[InstalledRemoteDependency],
|
|
106
116
|
mode_dependencies: list[str],
|
|
107
117
|
status: Status,
|
|
@@ -119,9 +129,14 @@ def _up(
|
|
|
119
129
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
120
130
|
] = relative_local_dependency_directory
|
|
121
131
|
options = ["-d"]
|
|
132
|
+
dependency_graph = construct_dependency_graph(service, modes=modes)
|
|
133
|
+
starting_order = dependency_graph.get_starting_order()
|
|
134
|
+
sorted_remote_dependencies = sorted(
|
|
135
|
+
remote_dependencies, key=lambda dep: starting_order.index(dep.service_name)
|
|
136
|
+
)
|
|
122
137
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
123
138
|
service=service,
|
|
124
|
-
remote_dependencies=
|
|
139
|
+
remote_dependencies=sorted_remote_dependencies,
|
|
125
140
|
current_env=current_env,
|
|
126
141
|
command="up",
|
|
127
142
|
options=options,
|
|
@@ -131,3 +146,11 @@ def _up(
|
|
|
131
146
|
|
|
132
147
|
for cmd in docker_compose_commands:
|
|
133
148
|
_bring_up_dependency(cmd, current_env, status, len(options))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _create_devservices_network() -> None:
|
|
152
|
+
subprocess.run(
|
|
153
|
+
["docker", "network", "create", "devservices"],
|
|
154
|
+
stdout=subprocess.DEVNULL,
|
|
155
|
+
stderr=subprocess.DEVNULL,
|
|
156
|
+
)
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
7
|
import tempfile
|
|
8
|
+
from collections import deque
|
|
8
9
|
from concurrent.futures import as_completed
|
|
9
10
|
from concurrent.futures import ThreadPoolExecutor
|
|
10
11
|
from dataclasses import dataclass
|
|
@@ -14,6 +15,7 @@ from typing import TypeGuard
|
|
|
14
15
|
from devservices.configs.service_config import Dependency
|
|
15
16
|
from devservices.configs.service_config import load_service_config_from_file
|
|
16
17
|
from devservices.configs.service_config import RemoteConfig
|
|
18
|
+
from devservices.configs.service_config import ServiceConfig
|
|
17
19
|
from devservices.constants import CONFIG_FILE_NAME
|
|
18
20
|
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
19
21
|
from devservices.constants import DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS
|
|
@@ -35,6 +37,62 @@ from devservices.utils.services import Service
|
|
|
35
37
|
from devservices.utils.state import State
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
class DependencyGraph:
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self.graph: dict[str, set[str]] = dict()
|
|
43
|
+
|
|
44
|
+
def add_dependency(self, service_name: str) -> None:
|
|
45
|
+
if service_name not in self.graph:
|
|
46
|
+
self.graph[service_name] = set()
|
|
47
|
+
|
|
48
|
+
def add_edge(self, service_name: str, dependency_name: str) -> None:
|
|
49
|
+
# TODO: We should rename services that depend on themselves
|
|
50
|
+
if service_name == dependency_name:
|
|
51
|
+
return
|
|
52
|
+
if service_name not in self.graph:
|
|
53
|
+
self.add_dependency(service_name)
|
|
54
|
+
if dependency_name not in self.graph:
|
|
55
|
+
self.add_dependency(dependency_name)
|
|
56
|
+
|
|
57
|
+
# TODO: Should we check for cycles here?
|
|
58
|
+
|
|
59
|
+
self.graph[service_name].add(dependency_name)
|
|
60
|
+
|
|
61
|
+
def topological_sort(self) -> list[str]:
|
|
62
|
+
in_degree = {service_name: 0 for service_name in self.graph}
|
|
63
|
+
|
|
64
|
+
for service_name in self.graph.keys():
|
|
65
|
+
for dependency in self.graph[service_name]:
|
|
66
|
+
in_degree[dependency] += 1
|
|
67
|
+
|
|
68
|
+
queue = deque(
|
|
69
|
+
[
|
|
70
|
+
service_name
|
|
71
|
+
for service_name in self.graph
|
|
72
|
+
if in_degree[service_name] == 0
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
topological_order = list()
|
|
76
|
+
|
|
77
|
+
while queue:
|
|
78
|
+
service_name = queue.popleft()
|
|
79
|
+
topological_order.append(service_name)
|
|
80
|
+
|
|
81
|
+
for dependency in self.graph[service_name]:
|
|
82
|
+
in_degree[dependency] -= 1
|
|
83
|
+
if in_degree[dependency] == 0:
|
|
84
|
+
queue.append(dependency)
|
|
85
|
+
|
|
86
|
+
if len(topological_order) != len(self.graph):
|
|
87
|
+
# TODO: Add a better exception
|
|
88
|
+
raise ValueError("Cycle detected in the dependency graph")
|
|
89
|
+
|
|
90
|
+
return topological_order
|
|
91
|
+
|
|
92
|
+
def get_starting_order(self) -> list[str]:
|
|
93
|
+
return list(reversed(self.topological_sort()))
|
|
94
|
+
|
|
95
|
+
|
|
38
96
|
@dataclass(frozen=True)
|
|
39
97
|
class InstalledRemoteDependency:
|
|
40
98
|
service_name: str
|
|
@@ -102,11 +160,20 @@ class GitConfigManager:
|
|
|
102
160
|
|
|
103
161
|
|
|
104
162
|
def install_and_verify_dependencies(
|
|
105
|
-
service: Service,
|
|
163
|
+
service: Service,
|
|
164
|
+
force_update_dependencies: bool = False,
|
|
165
|
+
modes: list[str] | None = None,
|
|
106
166
|
) -> set[InstalledRemoteDependency]:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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])
|
|
110
177
|
matching_dependencies = [
|
|
111
178
|
dependency
|
|
112
179
|
for dependency_key, dependency in list(service.config.dependencies.items())
|
|
@@ -288,8 +355,18 @@ def install_dependency(dependency: RemoteConfig) -> set[InstalledRemoteDependenc
|
|
|
288
355
|
branch=dependency.branch,
|
|
289
356
|
) from e
|
|
290
357
|
|
|
291
|
-
|
|
292
|
-
|
|
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)
|
|
293
370
|
|
|
294
371
|
installed_dependencies: set[InstalledRemoteDependency] = set(
|
|
295
372
|
[
|
|
@@ -457,3 +534,34 @@ def _run_command(
|
|
|
457
534
|
logger = logging.getLogger(LOGGER_NAME)
|
|
458
535
|
logger.debug(f"Running command: {' '.join(cmd)} in {cwd}")
|
|
459
536
|
subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
|
|
540
|
+
dependency_repo_dir = os.path.join(
|
|
541
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR,
|
|
542
|
+
DEPENDENCY_CONFIG_VERSION,
|
|
543
|
+
remote_config.repo_name,
|
|
544
|
+
)
|
|
545
|
+
return load_service_config_from_file(dependency_repo_dir)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def construct_dependency_graph(service: Service, modes: list[str]) -> DependencyGraph:
|
|
549
|
+
dependency_graph = DependencyGraph()
|
|
550
|
+
|
|
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, []))
|
|
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
|
|
561
|
+
dependency_graph.add_edge(service_config.service_name, dependency_name)
|
|
562
|
+
if _has_remote_config(dependency.remote):
|
|
563
|
+
dependency_config = get_remote_dependency_config(dependency.remote)
|
|
564
|
+
_construct_dependency_graph(dependency_config, [dependency.remote.mode])
|
|
565
|
+
|
|
566
|
+
_construct_dependency_graph(service.config, modes)
|
|
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")
|
|
@@ -163,7 +163,7 @@ def _get_non_remote_services(
|
|
|
163
163
|
|
|
164
164
|
def get_docker_compose_commands_to_run(
|
|
165
165
|
service: Service,
|
|
166
|
-
remote_dependencies:
|
|
166
|
+
remote_dependencies: list[InstalledRemoteDependency],
|
|
167
167
|
current_env: dict[str, str],
|
|
168
168
|
command: str,
|
|
169
169
|
options: list[str],
|
|
@@ -184,8 +184,7 @@ def get_docker_compose_commands_to_run(
|
|
|
184
184
|
+ sorted(list(services_to_use)) # Sort the services to prevent flaky tests
|
|
185
185
|
+ options
|
|
186
186
|
)
|
|
187
|
-
|
|
188
|
-
for dependency in sorted(remote_dependencies, key=lambda x: x.service_name):
|
|
187
|
+
for dependency in remote_dependencies:
|
|
189
188
|
# TODO: Consider passing in service config in InstalledRemoteDependency instead of loading it here
|
|
190
189
|
dependency_service_config = load_service_config_from_file(dependency.repo_path)
|
|
191
190
|
dependency_config_path = os.path.join(
|
|
@@ -35,17 +35,26 @@ class State:
|
|
|
35
35
|
)
|
|
36
36
|
self.conn.commit()
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def update_started_service(self, service_name: str, mode: str) -> None:
|
|
39
39
|
cursor = self.conn.cursor()
|
|
40
40
|
started_services = self.get_started_services()
|
|
41
|
-
|
|
41
|
+
active_modes = self.get_active_modes_for_service(service_name)
|
|
42
|
+
if service_name in started_services and mode in active_modes:
|
|
42
43
|
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if service_name in started_services:
|
|
45
|
+
cursor.execute(
|
|
46
|
+
"""
|
|
47
|
+
UPDATE started_services SET mode = ? WHERE service_name = ?
|
|
48
|
+
""",
|
|
49
|
+
(",".join(active_modes + [mode]), service_name),
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
cursor.execute(
|
|
53
|
+
"""
|
|
54
|
+
INSERT INTO started_services (service_name, mode) VALUES (?, ?)
|
|
55
|
+
""",
|
|
56
|
+
(service_name, ",".join(active_modes + [mode])),
|
|
57
|
+
)
|
|
49
58
|
self.conn.commit()
|
|
50
59
|
|
|
51
60
|
def remove_started_service(self, service_name: str) -> None:
|
|
@@ -67,7 +76,7 @@ class State:
|
|
|
67
76
|
)
|
|
68
77
|
return [row[0] for row in cursor.fetchall()]
|
|
69
78
|
|
|
70
|
-
def
|
|
79
|
+
def get_active_modes_for_service(self, service_name: str) -> list[str]:
|
|
71
80
|
cursor = self.conn.cursor()
|
|
72
81
|
cursor.execute(
|
|
73
82
|
"""
|
|
@@ -77,8 +86,8 @@ class State:
|
|
|
77
86
|
)
|
|
78
87
|
result = cursor.fetchone()
|
|
79
88
|
if result is None:
|
|
80
|
-
return
|
|
81
|
-
return str(result[0])
|
|
89
|
+
return []
|
|
90
|
+
return str(result[0]).split(",")
|
|
82
91
|
|
|
83
92
|
def clear_state(self) -> None:
|
|
84
93
|
cursor = self.conn.cursor()
|
|
@@ -38,9 +38,11 @@ testing/utils.py
|
|
|
38
38
|
tests/__init__.py
|
|
39
39
|
tests/conftest.py
|
|
40
40
|
tests/commands/test_down.py
|
|
41
|
+
tests/commands/test_list_dependencies.py
|
|
41
42
|
tests/commands/test_list_services.py
|
|
42
43
|
tests/commands/test_logs.py
|
|
43
44
|
tests/commands/test_purge.py
|
|
45
|
+
tests/commands/test_status.py
|
|
44
46
|
tests/commands/test_up.py
|
|
45
47
|
tests/commands/test_update.py
|
|
46
48
|
tests/configs/test_service_config.py
|
|
@@ -48,4 +50,5 @@ tests/utils/test_dependencies.py
|
|
|
48
50
|
tests/utils/test_docker.py
|
|
49
51
|
tests/utils/test_docker_compose.py
|
|
50
52
|
tests/utils/test_install_binary.py
|
|
53
|
+
tests/utils/test_services.py
|
|
51
54
|
tests/utils/test_state.py
|
|
@@ -13,6 +13,8 @@ from devservices.constants import CONFIG_FILE_NAME
|
|
|
13
13
|
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
14
14
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
15
15
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
16
|
+
from devservices.exceptions import ConfigError
|
|
17
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
16
18
|
from devservices.utils.state import State
|
|
17
19
|
from testing.utils import create_config_file
|
|
18
20
|
|
|
@@ -64,7 +66,7 @@ def test_down_simple(
|
|
|
64
66
|
"devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")
|
|
65
67
|
):
|
|
66
68
|
state = State()
|
|
67
|
-
state.
|
|
69
|
+
state.update_started_service("example-service", "default")
|
|
68
70
|
down(args)
|
|
69
71
|
|
|
70
72
|
# Ensure the DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY is set and is relative
|
|
@@ -135,7 +137,7 @@ def test_down_error(
|
|
|
135
137
|
|
|
136
138
|
with mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
|
|
137
139
|
state = State()
|
|
138
|
-
state.
|
|
140
|
+
state.update_started_service("example-service", "default")
|
|
139
141
|
with pytest.raises(SystemExit):
|
|
140
142
|
down(args)
|
|
141
143
|
|
|
@@ -200,7 +202,7 @@ def test_down_mode_simple(
|
|
|
200
202
|
"devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")
|
|
201
203
|
):
|
|
202
204
|
state = State()
|
|
203
|
-
state.
|
|
205
|
+
state.update_started_service("example-service", "test")
|
|
204
206
|
down(args)
|
|
205
207
|
|
|
206
208
|
# Ensure the DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY is set and is relative
|
|
@@ -231,3 +233,33 @@ def test_down_mode_simple(
|
|
|
231
233
|
|
|
232
234
|
captured = capsys.readouterr()
|
|
233
235
|
assert "Stopping redis" in captured.out.strip()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@mock.patch("devservices.commands.down.find_matching_service")
|
|
239
|
+
def test_down_config_error(
|
|
240
|
+
find_matching_service_mock: mock.Mock, capsys: pytest.CaptureFixture[str]
|
|
241
|
+
) -> None:
|
|
242
|
+
find_matching_service_mock.side_effect = ConfigError("Config error")
|
|
243
|
+
args = Namespace(service_name="example-service", debug=False)
|
|
244
|
+
|
|
245
|
+
with pytest.raises(SystemExit):
|
|
246
|
+
down(args)
|
|
247
|
+
|
|
248
|
+
find_matching_service_mock.assert_called_once_with("example-service")
|
|
249
|
+
captured = capsys.readouterr()
|
|
250
|
+
assert "Config error" in captured.out.strip()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@mock.patch("devservices.commands.down.find_matching_service")
|
|
254
|
+
def test_down_service_not_found_error(
|
|
255
|
+
find_matching_service_mock: mock.Mock, capsys: pytest.CaptureFixture[str]
|
|
256
|
+
) -> None:
|
|
257
|
+
find_matching_service_mock.side_effect = ServiceNotFoundError("Service not found")
|
|
258
|
+
args = Namespace(service_name="example-service", debug=False)
|
|
259
|
+
|
|
260
|
+
with pytest.raises(SystemExit):
|
|
261
|
+
down(args)
|
|
262
|
+
|
|
263
|
+
find_matching_service_mock.assert_called_once_with("example-service")
|
|
264
|
+
captured = capsys.readouterr()
|
|
265
|
+
assert "Service not found" in captured.out.strip()
|