devservices 1.0.3__tar.gz → 1.0.4__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.4}/PKG-INFO +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/README.md +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/down.py +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/logs.py +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/status.py +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/up.py +73 -2
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/dependencies.py +81 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/docker_compose.py +2 -3
- {devservices-1.0.3 → devservices-1.0.4}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/pyproject.toml +1 -1
- {devservices-1.0.3 → devservices-1.0.4}/tests/commands/test_up.py +263 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/utils/test_dependencies.py +422 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/utils/test_docker_compose.py +127 -22
- {devservices-1.0.3 → devservices-1.0.4}/LICENSE.md +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/check_for_update.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/list_services.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/purge.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/commands/update.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/constants.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/exceptions.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/main.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/console.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/devenv.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/docker.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/services.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices/utils/state.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices.egg-info/SOURCES.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/setup.cfg +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/testing/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/testing/utils.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/__init__.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/commands/test_down.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/commands/test_list_services.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/commands/test_logs.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/commands/test_purge.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/commands/test_update.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/conftest.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/utils/test_docker.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.0.3 → devservices-1.0.4}/tests/utils/test_state.py +0 -0
|
@@ -126,7 +126,7 @@ def _down(
|
|
|
126
126
|
] = relative_local_dependency_directory
|
|
127
127
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
128
128
|
service=service,
|
|
129
|
-
remote_dependencies=remote_dependencies,
|
|
129
|
+
remote_dependencies=list(remote_dependencies),
|
|
130
130
|
current_env=current_env,
|
|
131
131
|
command="down",
|
|
132
132
|
options=[],
|
|
@@ -99,7 +99,7 @@ def _logs(
|
|
|
99
99
|
] = relative_local_dependency_directory
|
|
100
100
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
101
101
|
service=service,
|
|
102
|
-
remote_dependencies=remote_dependencies,
|
|
102
|
+
remote_dependencies=list(remote_dependencies),
|
|
103
103
|
current_env=current_env,
|
|
104
104
|
command="logs",
|
|
105
105
|
options=["-n", MAX_LOG_LINES],
|
|
@@ -137,7 +137,7 @@ def _status(
|
|
|
137
137
|
] = relative_local_dependency_directory
|
|
138
138
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
139
139
|
service=service,
|
|
140
|
-
remote_dependencies=remote_dependencies,
|
|
140
|
+
remote_dependencies=list(remote_dependencies),
|
|
141
141
|
current_env=current_env,
|
|
142
142
|
command="ps",
|
|
143
143
|
options=["--format", "json"],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import concurrent.futures
|
|
3
4
|
import os
|
|
4
5
|
import subprocess
|
|
5
6
|
from argparse import _SubParsersAction
|
|
@@ -21,6 +22,8 @@ from devservices.exceptions import ModeDoesNotExistError
|
|
|
21
22
|
from devservices.exceptions import ServiceNotFoundError
|
|
22
23
|
from devservices.utils.console import Console
|
|
23
24
|
from devservices.utils.console import Status
|
|
25
|
+
from devservices.utils.dependencies import construct_dependency_graph
|
|
26
|
+
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
24
27
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
25
28
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
26
29
|
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
@@ -63,8 +66,58 @@ def up(args: Namespace) -> None:
|
|
|
63
66
|
modes = service.config.modes
|
|
64
67
|
mode = args.mode
|
|
65
68
|
|
|
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
|
+
|
|
66
119
|
with Status(
|
|
67
|
-
lambda: console.warning(f"Starting {service.name}"),
|
|
120
|
+
lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
|
|
68
121
|
lambda: console.success(f"{service.name} started"),
|
|
69
122
|
) as status:
|
|
70
123
|
try:
|
|
@@ -79,6 +132,11 @@ def up(args: Namespace) -> None:
|
|
|
79
132
|
except ModeDoesNotExistError as mde:
|
|
80
133
|
status.failure(str(mde))
|
|
81
134
|
exit(1)
|
|
135
|
+
try:
|
|
136
|
+
_create_devservices_network()
|
|
137
|
+
except subprocess.CalledProcessError:
|
|
138
|
+
# Network already exists, ignore the error
|
|
139
|
+
pass
|
|
82
140
|
try:
|
|
83
141
|
mode_dependencies = modes[mode]
|
|
84
142
|
_up(service, remote_dependencies, mode_dependencies, status)
|
|
@@ -119,9 +177,14 @@ def _up(
|
|
|
119
177
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
120
178
|
] = relative_local_dependency_directory
|
|
121
179
|
options = ["-d"]
|
|
180
|
+
dependency_graph = construct_dependency_graph(service)
|
|
181
|
+
starting_order = dependency_graph.get_starting_order()
|
|
182
|
+
sorted_remote_dependencies = sorted(
|
|
183
|
+
remote_dependencies, key=lambda dep: starting_order.index(dep.service_name)
|
|
184
|
+
)
|
|
122
185
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
123
186
|
service=service,
|
|
124
|
-
remote_dependencies=
|
|
187
|
+
remote_dependencies=sorted_remote_dependencies,
|
|
125
188
|
current_env=current_env,
|
|
126
189
|
command="up",
|
|
127
190
|
options=options,
|
|
@@ -131,3 +194,11 @@ def _up(
|
|
|
131
194
|
|
|
132
195
|
for cmd in docker_compose_commands:
|
|
133
196
|
_bring_up_dependency(cmd, current_env, status, len(options))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _create_devservices_network() -> None:
|
|
200
|
+
subprocess.run(
|
|
201
|
+
["docker", "network", "create", "devservices"],
|
|
202
|
+
stdout=subprocess.DEVNULL,
|
|
203
|
+
stderr=subprocess.DEVNULL,
|
|
204
|
+
)
|
|
@@ -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
|
|
@@ -457,3 +515,26 @@ def _run_command(
|
|
|
457
515
|
logger = logging.getLogger(LOGGER_NAME)
|
|
458
516
|
logger.debug(f"Running command: {' '.join(cmd)} in {cwd}")
|
|
459
517
|
subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
|
|
521
|
+
dependency_repo_dir = os.path.join(
|
|
522
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR,
|
|
523
|
+
DEPENDENCY_CONFIG_VERSION,
|
|
524
|
+
remote_config.repo_name,
|
|
525
|
+
)
|
|
526
|
+
return load_service_config_from_file(dependency_repo_dir)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def construct_dependency_graph(service: Service) -> DependencyGraph:
|
|
530
|
+
dependency_graph = DependencyGraph()
|
|
531
|
+
|
|
532
|
+
def _construct_dependency_graph(service_config: ServiceConfig) -> None:
|
|
533
|
+
for dependency_name, dependency in service_config.dependencies.items():
|
|
534
|
+
dependency_graph.add_edge(service_config.service_name, dependency_name)
|
|
535
|
+
if _has_remote_config(dependency.remote):
|
|
536
|
+
dependency_config = get_remote_dependency_config(dependency.remote)
|
|
537
|
+
_construct_dependency_graph(dependency_config)
|
|
538
|
+
|
|
539
|
+
_construct_dependency_graph(service.config)
|
|
540
|
+
return dependency_graph
|
|
@@ -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(
|
|
@@ -14,7 +14,10 @@ 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
16
|
from devservices.exceptions import DependencyError
|
|
17
|
+
from devservices.utils.state import State
|
|
17
18
|
from testing.utils import create_config_file
|
|
19
|
+
from testing.utils import create_mock_git_repo
|
|
20
|
+
from testing.utils import run_git_command
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
@mock.patch(
|
|
@@ -26,7 +29,9 @@ from testing.utils import create_config_file
|
|
|
26
29
|
),
|
|
27
30
|
)
|
|
28
31
|
@mock.patch("devservices.utils.state.State.add_started_service")
|
|
32
|
+
@mock.patch("devservices.commands.up._create_devservices_network")
|
|
29
33
|
def test_up_simple(
|
|
34
|
+
mock_create_devservices_network: mock.Mock,
|
|
30
35
|
mock_add_started_service: mock.Mock,
|
|
31
36
|
mock_run: mock.Mock,
|
|
32
37
|
tmp_path: Path,
|
|
@@ -69,6 +74,8 @@ def test_up_simple(
|
|
|
69
74
|
== f"../dependency-dir/{DEPENDENCY_CONFIG_VERSION}"
|
|
70
75
|
)
|
|
71
76
|
|
|
77
|
+
mock_create_devservices_network.assert_called_once()
|
|
78
|
+
|
|
72
79
|
mock_run.assert_called_with(
|
|
73
80
|
[
|
|
74
81
|
"docker",
|
|
@@ -91,13 +98,16 @@ def test_up_simple(
|
|
|
91
98
|
mock_add_started_service.assert_called_with("example-service", "default")
|
|
92
99
|
captured = capsys.readouterr()
|
|
93
100
|
assert "Retrieving dependencies" in captured.out.strip()
|
|
101
|
+
assert "Starting 'example-service' in mode: 'default'" in captured.out.strip()
|
|
94
102
|
assert "Starting clickhouse" in captured.out.strip()
|
|
95
103
|
assert "Starting redis" in captured.out.strip()
|
|
96
104
|
|
|
97
105
|
|
|
98
106
|
@mock.patch("devservices.utils.docker_compose.subprocess.run")
|
|
99
107
|
@mock.patch("devservices.utils.state.State.add_started_service")
|
|
108
|
+
@mock.patch("devservices.commands.up._create_devservices_network")
|
|
100
109
|
def test_up_dependency_error(
|
|
110
|
+
mock_create_devservices_network: mock.Mock,
|
|
101
111
|
mock_add_started_service: mock.Mock,
|
|
102
112
|
mock_run: mock.Mock,
|
|
103
113
|
capsys: pytest.CaptureFixture[str],
|
|
@@ -135,6 +145,7 @@ def test_up_dependency_error(
|
|
|
135
145
|
with pytest.raises(SystemExit):
|
|
136
146
|
up(args)
|
|
137
147
|
|
|
148
|
+
mock_create_devservices_network.assert_not_called()
|
|
138
149
|
# Capture the printed output
|
|
139
150
|
captured = capsys.readouterr()
|
|
140
151
|
|
|
@@ -144,13 +155,18 @@ def test_up_dependency_error(
|
|
|
144
155
|
|
|
145
156
|
captured = capsys.readouterr()
|
|
146
157
|
assert "Retrieving dependencies" not in captured.out.strip()
|
|
158
|
+
assert (
|
|
159
|
+
"Starting 'example-service' in mode: 'default'" not in captured.out.strip()
|
|
160
|
+
)
|
|
147
161
|
assert "Starting clickhouse" not in captured.out.strip()
|
|
148
162
|
assert "Starting redis" not in captured.out.strip()
|
|
149
163
|
|
|
150
164
|
|
|
151
165
|
@mock.patch("devservices.utils.docker_compose.subprocess.run")
|
|
152
166
|
@mock.patch("devservices.utils.state.State.add_started_service")
|
|
167
|
+
@mock.patch("devservices.commands.up._create_devservices_network")
|
|
153
168
|
def test_up_error(
|
|
169
|
+
mock_create_devservices_network: mock.Mock,
|
|
154
170
|
mock_add_started_service: mock.Mock,
|
|
155
171
|
mock_run: mock.Mock,
|
|
156
172
|
capsys: pytest.CaptureFixture[str],
|
|
@@ -185,6 +201,8 @@ def test_up_error(
|
|
|
185
201
|
with pytest.raises(SystemExit):
|
|
186
202
|
up(args)
|
|
187
203
|
|
|
204
|
+
mock_create_devservices_network.assert_called_once()
|
|
205
|
+
|
|
188
206
|
# Capture the printed output
|
|
189
207
|
captured = capsys.readouterr()
|
|
190
208
|
|
|
@@ -196,6 +214,7 @@ def test_up_error(
|
|
|
196
214
|
|
|
197
215
|
captured = capsys.readouterr()
|
|
198
216
|
assert "Retrieving dependencies" not in captured.out.strip()
|
|
217
|
+
assert "Starting 'example-service' in mode: 'default'" not in captured.out.strip()
|
|
199
218
|
assert "Starting clickhouse" not in captured.out.strip()
|
|
200
219
|
assert "Starting redis" not in captured.out.strip()
|
|
201
220
|
|
|
@@ -209,7 +228,9 @@ def test_up_error(
|
|
|
209
228
|
),
|
|
210
229
|
)
|
|
211
230
|
@mock.patch("devservices.utils.state.State.add_started_service")
|
|
231
|
+
@mock.patch("devservices.commands.up._create_devservices_network")
|
|
212
232
|
def test_up_mode_simple(
|
|
233
|
+
mock_create_devservices_network: mock.Mock,
|
|
213
234
|
mock_add_started_service: mock.Mock,
|
|
214
235
|
mock_run: mock.Mock,
|
|
215
236
|
tmp_path: Path,
|
|
@@ -252,6 +273,8 @@ def test_up_mode_simple(
|
|
|
252
273
|
== f"../dependency-dir/{DEPENDENCY_CONFIG_VERSION}"
|
|
253
274
|
)
|
|
254
275
|
|
|
276
|
+
mock_create_devservices_network.assert_called_once()
|
|
277
|
+
|
|
255
278
|
mock_run.assert_called_with(
|
|
256
279
|
[
|
|
257
280
|
"docker",
|
|
@@ -273,6 +296,7 @@ def test_up_mode_simple(
|
|
|
273
296
|
mock_add_started_service.assert_called_with("example-service", "test")
|
|
274
297
|
captured = capsys.readouterr()
|
|
275
298
|
assert "Retrieving dependencies" in captured.out.strip()
|
|
299
|
+
assert "Starting 'example-service' in mode: 'test'" in captured.out.strip()
|
|
276
300
|
assert "Starting redis" in captured.out.strip()
|
|
277
301
|
|
|
278
302
|
|
|
@@ -334,5 +358,244 @@ def test_up_mode_does_not_exist(
|
|
|
334
358
|
|
|
335
359
|
captured = capsys.readouterr()
|
|
336
360
|
assert "Retrieving dependencies" not in captured.out.strip()
|
|
361
|
+
assert "Starting 'example-service' in mode: 'test'" not in captured.out.strip()
|
|
337
362
|
assert "Starting clickhouse" not in captured.out.strip()
|
|
338
363
|
assert "Starting redis" not in captured.out.strip()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mock.patch(
|
|
367
|
+
"devservices.utils.docker_compose.subprocess.run",
|
|
368
|
+
return_value=subprocess.CompletedProcess(
|
|
369
|
+
args=["docker", "compose", "config", "--services"],
|
|
370
|
+
returncode=0,
|
|
371
|
+
stdout="clickhouse\nredis\n",
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
def test_up_switching_modes(
|
|
375
|
+
mock_run: mock.Mock,
|
|
376
|
+
tmp_path: Path,
|
|
377
|
+
capsys: pytest.CaptureFixture[str],
|
|
378
|
+
) -> None:
|
|
379
|
+
with (
|
|
380
|
+
mock.patch(
|
|
381
|
+
"devservices.commands.up.DEVSERVICES_DEPENDENCIES_CACHE_DIR",
|
|
382
|
+
str(tmp_path / "dependency-dir"),
|
|
383
|
+
),
|
|
384
|
+
mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
|
|
385
|
+
):
|
|
386
|
+
config = {
|
|
387
|
+
"x-sentry-service-config": {
|
|
388
|
+
"version": 0.1,
|
|
389
|
+
"service_name": "example-service",
|
|
390
|
+
"dependencies": {
|
|
391
|
+
"redis": {"description": "Redis"},
|
|
392
|
+
"clickhouse": {"description": "Clickhouse"},
|
|
393
|
+
},
|
|
394
|
+
"modes": {"default": ["redis", "clickhouse"], "test": ["redis"]},
|
|
395
|
+
},
|
|
396
|
+
"services": {
|
|
397
|
+
"redis": {"image": "redis:6.2.14-alpine"},
|
|
398
|
+
"clickhouse": {
|
|
399
|
+
"image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
service_path = tmp_path / "example-service"
|
|
405
|
+
create_config_file(service_path, config)
|
|
406
|
+
os.chdir(service_path)
|
|
407
|
+
|
|
408
|
+
state = State()
|
|
409
|
+
state.add_started_service("example-service", "default")
|
|
410
|
+
|
|
411
|
+
args = Namespace(service_name=None, debug=False, mode="test")
|
|
412
|
+
up(args)
|
|
413
|
+
|
|
414
|
+
mock_run.assert_has_calls(
|
|
415
|
+
[
|
|
416
|
+
mock.call(
|
|
417
|
+
[
|
|
418
|
+
"docker",
|
|
419
|
+
"compose",
|
|
420
|
+
"-p",
|
|
421
|
+
"example-service",
|
|
422
|
+
"-f",
|
|
423
|
+
f"{service_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
|
|
424
|
+
"down",
|
|
425
|
+
"clickhouse",
|
|
426
|
+
"redis",
|
|
427
|
+
],
|
|
428
|
+
check=True,
|
|
429
|
+
capture_output=True,
|
|
430
|
+
text=True,
|
|
431
|
+
env=mock.ANY,
|
|
432
|
+
),
|
|
433
|
+
mock.call(
|
|
434
|
+
[
|
|
435
|
+
"docker",
|
|
436
|
+
"compose",
|
|
437
|
+
"-p",
|
|
438
|
+
"example-service",
|
|
439
|
+
"-f",
|
|
440
|
+
f"{service_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
|
|
441
|
+
"up",
|
|
442
|
+
"redis",
|
|
443
|
+
"-d",
|
|
444
|
+
],
|
|
445
|
+
check=True,
|
|
446
|
+
capture_output=True,
|
|
447
|
+
text=True,
|
|
448
|
+
env=mock.ANY,
|
|
449
|
+
),
|
|
450
|
+
],
|
|
451
|
+
any_order=True,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
captured = capsys.readouterr()
|
|
455
|
+
assert (
|
|
456
|
+
"Service 'example-service' is already running in mode: 'default', restarting in mode: 'test'"
|
|
457
|
+
in captured.out.strip()
|
|
458
|
+
)
|
|
459
|
+
assert "Starting 'example-service' in mode: 'test'" in captured.out.strip()
|
|
460
|
+
assert "Retrieving dependencies" in captured.out.strip()
|
|
461
|
+
assert "Starting redis" in captured.out.strip()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_up_switching_modes_overlapping_running_service(
|
|
465
|
+
tmp_path: Path,
|
|
466
|
+
capsys: pytest.CaptureFixture[str],
|
|
467
|
+
) -> None:
|
|
468
|
+
with (
|
|
469
|
+
mock.patch(
|
|
470
|
+
"devservices.commands.up.DEVSERVICES_DEPENDENCIES_CACHE_DIR",
|
|
471
|
+
str(tmp_path / "dependency-dir"),
|
|
472
|
+
),
|
|
473
|
+
mock.patch(
|
|
474
|
+
"devservices.utils.dependencies.DEVSERVICES_DEPENDENCIES_CACHE_DIR",
|
|
475
|
+
str(tmp_path / "dependency-dir"),
|
|
476
|
+
),
|
|
477
|
+
mock.patch(
|
|
478
|
+
"devservices.utils.services.get_coderoot",
|
|
479
|
+
return_value=str(tmp_path / "code"),
|
|
480
|
+
),
|
|
481
|
+
mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
|
|
482
|
+
):
|
|
483
|
+
redis_repo_path = tmp_path / "redis"
|
|
484
|
+
create_mock_git_repo("blank_repo", redis_repo_path)
|
|
485
|
+
mock_git_repo_config = {
|
|
486
|
+
"x-sentry-service-config": {
|
|
487
|
+
"version": 0.1,
|
|
488
|
+
"service_name": "shared-redis",
|
|
489
|
+
"dependencies": {},
|
|
490
|
+
"modes": {"default": []},
|
|
491
|
+
},
|
|
492
|
+
"services": {
|
|
493
|
+
"redis": {"image": "redis:6.2.14-alpine"},
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
create_config_file(redis_repo_path, mock_git_repo_config)
|
|
497
|
+
run_git_command(["add", "."], cwd=redis_repo_path)
|
|
498
|
+
run_git_command(["commit", "-m", "Add devservices config"], cwd=redis_repo_path)
|
|
499
|
+
config = {
|
|
500
|
+
"x-sentry-service-config": {
|
|
501
|
+
"version": 0.1,
|
|
502
|
+
"service_name": "example-service",
|
|
503
|
+
"dependencies": {
|
|
504
|
+
"redis": {
|
|
505
|
+
"description": "Redis",
|
|
506
|
+
"remote": {
|
|
507
|
+
"repo_name": "redis",
|
|
508
|
+
"branch": "main",
|
|
509
|
+
"repo_link": f"file://{redis_repo_path}",
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
"clickhouse": {"description": "Clickhouse"},
|
|
513
|
+
},
|
|
514
|
+
"modes": {"default": ["redis", "clickhouse"], "test": ["clickhouse"]},
|
|
515
|
+
},
|
|
516
|
+
"services": {
|
|
517
|
+
"clickhouse": {
|
|
518
|
+
"image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
other_config = {
|
|
523
|
+
"x-sentry-service-config": {
|
|
524
|
+
"version": 0.1,
|
|
525
|
+
"service_name": "other-service",
|
|
526
|
+
"dependencies": {
|
|
527
|
+
"redis": {
|
|
528
|
+
"description": "Redis",
|
|
529
|
+
"remote": {
|
|
530
|
+
"repo_name": "redis",
|
|
531
|
+
"branch": "main",
|
|
532
|
+
"repo_link": f"file://{redis_repo_path}",
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
"modes": {"default": ["redis"]},
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
service_path = tmp_path / "code" / "example-service"
|
|
541
|
+
other_service_path = tmp_path / "code" / "other-service"
|
|
542
|
+
create_config_file(service_path, config)
|
|
543
|
+
create_config_file(other_service_path, other_config)
|
|
544
|
+
os.chdir(service_path)
|
|
545
|
+
|
|
546
|
+
state = State()
|
|
547
|
+
state.add_started_service("example-service", "default")
|
|
548
|
+
state.add_started_service("other-service", "default")
|
|
549
|
+
|
|
550
|
+
args = Namespace(service_name="example-service", debug=False, mode="test")
|
|
551
|
+
|
|
552
|
+
with mock.patch(
|
|
553
|
+
"devservices.commands.up.run_cmd",
|
|
554
|
+
return_value=subprocess.CompletedProcess(
|
|
555
|
+
args=["docker", "compose", "config", "--services"],
|
|
556
|
+
returncode=0,
|
|
557
|
+
stdout="clickhouse\n",
|
|
558
|
+
),
|
|
559
|
+
) as mock_run:
|
|
560
|
+
up(args)
|
|
561
|
+
|
|
562
|
+
mock_run.assert_has_calls(
|
|
563
|
+
[
|
|
564
|
+
mock.call(
|
|
565
|
+
[
|
|
566
|
+
"docker",
|
|
567
|
+
"compose",
|
|
568
|
+
"-p",
|
|
569
|
+
"example-service",
|
|
570
|
+
"-f",
|
|
571
|
+
f"{service_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
|
|
572
|
+
"down",
|
|
573
|
+
"clickhouse",
|
|
574
|
+
],
|
|
575
|
+
mock.ANY,
|
|
576
|
+
),
|
|
577
|
+
mock.call(
|
|
578
|
+
[
|
|
579
|
+
"docker",
|
|
580
|
+
"compose",
|
|
581
|
+
"-p",
|
|
582
|
+
"example-service",
|
|
583
|
+
"-f",
|
|
584
|
+
f"{service_path}/{DEVSERVICES_DIR_NAME}/{CONFIG_FILE_NAME}",
|
|
585
|
+
"up",
|
|
586
|
+
"clickhouse",
|
|
587
|
+
"-d",
|
|
588
|
+
],
|
|
589
|
+
mock.ANY,
|
|
590
|
+
),
|
|
591
|
+
],
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
captured = capsys.readouterr()
|
|
595
|
+
assert (
|
|
596
|
+
"Service 'example-service' is already running in mode: 'default', restarting in mode: 'test'"
|
|
597
|
+
in captured.out.strip()
|
|
598
|
+
)
|
|
599
|
+
assert "Starting 'example-service' in mode: 'test'" in captured.out.strip()
|
|
600
|
+
assert "Retrieving dependencies" in captured.out.strip()
|
|
601
|
+
assert "Starting clickhouse" in captured.out.strip()
|